lockbox 0.4.8 → 0.6.2

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: a560c020c3adf21952f81767ffc9b5b4586784f62d748f484e7bacbd4076a64a
4
- data.tar.gz: 59d05b405b4cd46da679ef4f03a53fae03cc78d7cdfe89bab13cd6981b76a4da
3
+ metadata.gz: 4ba37bc916c02e18555640f29e47483898a96e04b75639b49ce9a0003fbaa443
4
+ data.tar.gz: 750a53ca3201e51b6dc0221305d4c021d64716a163a2e0cfc98c11ec8d1b6af8
5
5
  SHA512:
6
- metadata.gz: 8d6217f47cc9c38ad8cf3db11b2a3a2936950b97f91ea168c5f2e4f8a1d9a5916c832286f08156869fbecf89d05dfc9bd7c4ecade9b9b4384488c936a292a1a6
7
- data.tar.gz: 3ddf36244c68b6b0bebad62801366d9827e6bee520717f1d544cfc6a18e798c644a158b68ac295fa87cef45ce5b922f37e89c5e39a5882ccb9fe512e725e778b
6
+ metadata.gz: b4e23752c311bf6b161e6817ab204ba0b5936594db5c2509c3f5ef6e354c169097a1c799d7b5147daa0d68982f072d1d22363019c6881566403517f97a5f2c45
7
+ data.tar.gz: 51ab913facdc34aea3e3ef263dab2fa5c3852aa052de80804ec29019c2517df9244eefb8c15e6f2c1ca0059bfd4f9cc020ce00fff543874f6461faeec1e78443
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## 0.6.2 (2020-02-08)
2
+
3
+ - Added `inet` type
4
+ - Fixed error when `lockbox` key in Rails credentials has a string value
5
+ - Fixed deprecation warning with Active Record 6.1
6
+
7
+ ## 0.6.1 (2020-12-03)
8
+
9
+ - Added integration with Rails credentials
10
+ - Fixed in place changes for Active Record 6.1
11
+ - Fixed error with `content_type` method for CarrierWave < 2
12
+
13
+ ## 0.6.0 (2020-12-03)
14
+
15
+ - Added `encrypted` flag to Active Storage metadata
16
+ - Added encrypted columns to `filter_attributes`
17
+ - Improved `inspect` method
18
+
19
+ ## 0.5.0 (2020-11-22)
20
+
21
+ - Improved error messages for hybrid cryptography
22
+ - Changed warning to error when no attributes specified
23
+ - Fixed issue with `pluck` when migrating
24
+ - Fixed error with `key_table` and `key_attribute` options with `previous_versions`
25
+
26
+ ## 0.4.9 (2020-10-01)
27
+
28
+ - Added `key_table` and `key_attribute` options to `previous_versions`
29
+ - Added `encrypted_attribute` option
30
+ - Added support for encrypting empty string
31
+ - Improved `inspect` for models with encrypted attributes
32
+
1
33
  ## 0.4.8 (2020-08-30)
2
34
 
3
35
  - Added `key_table` and `key_attribute` options
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018-2020 Andrew Kane
3
+ Copyright (c) 2018-2021 Andrew Kane
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # Lockbox
2
2
 
3
- :package: Modern encryption for Rails
3
+ :package: Modern encryption for Ruby and Rails
4
4
 
5
5
  - Works with database fields, files, and strings
6
6
  - Maximizes compatibility with existing code and libraries
7
7
  - Makes migrating existing data and key rotation easy
8
+ - Has zero dependencies and many integrations
8
9
 
9
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).
10
11
 
11
- [![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)
12
13
 
13
14
  ## Installation
14
15
 
@@ -26,7 +27,7 @@ Generate a key
26
27
  Lockbox.generate_key
27
28
  ```
28
29
 
29
- Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production. Keys don’t need to be hex-encoded, but it’s often easier to store them this way.
30
+ Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production.
30
31
 
31
32
  Set the following environment variable with your key (you can use this one in development)
32
33
 
@@ -34,10 +35,17 @@ Set the following environment variable with your key (you can use this one in de
34
35
  LOCKBOX_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
35
36
  ```
36
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
+
37
45
  or create `config/initializers/lockbox.rb` with something like
38
46
 
39
47
  ```ruby
40
- Lockbox.master_key = Rails.application.credentials.lockbox_master_key
48
+ Lockbox.master_key = Rails.application.credentials.lockbox[:master_key]
41
49
  ```
42
50
 
43
51
  Then follow the instructions below for the data you want to encrypt.
@@ -113,6 +121,7 @@ class User < ApplicationRecord
113
121
  encrypts :properties, type: :json
114
122
  encrypts :settings, type: :hash
115
123
  encrypts :messages, type: :array
124
+ encrypts :ip, type: :inet
116
125
  end
117
126
  ```
118
127
 
@@ -196,6 +205,34 @@ class User < ApplicationRecord
196
205
  end
197
206
  ```
198
207
 
208
+ #### Model Changes
209
+
210
+ If tracking changes to model attributes, be sure to remove or redact encrypted attributes.
211
+
212
+ PaperTrail
213
+
214
+ ```ruby
215
+ class User < ApplicationRecord
216
+ # for an encrypted history (still tracks ciphertext changes)
217
+ has_paper_trail skip: [:email]
218
+
219
+ # for no history (add blind indexes as well)
220
+ has_paper_trail skip: [:email, :email_ciphertext]
221
+ end
222
+ ```
223
+
224
+ Audited
225
+
226
+ ```ruby
227
+ class User < ApplicationRecord
228
+ # for an encrypted history (still tracks ciphertext changes)
229
+ audited except: [:email]
230
+
231
+ # for no history (add blind indexes as well)
232
+ audited except: [:email, :email_ciphertext]
233
+ end
234
+ ```
235
+
199
236
  #### Decryption
200
237
 
201
238
  To decrypt data outside the model, use:
@@ -413,44 +450,58 @@ Finally, delete the unencrypted files and drop the column for the original uploa
413
450
 
414
451
  ## Shrine
415
452
 
416
- Generate a key
453
+ #### Models
454
+
455
+ Include the attachment as normal:
417
456
 
418
457
  ```ruby
419
- key = Lockbox.generate_key
458
+ class User < ApplicationRecord
459
+ include LicenseUploader::Attachment(:license)
460
+ end
420
461
  ```
421
462
 
422
- Create a lockbox
463
+ And encrypt in a controller (or background job, etc) with:
423
464
 
424
465
  ```ruby
425
- lockbox = Lockbox.new(key: key)
466
+ license = params.require(:user).fetch(:license)
467
+ lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
468
+ user.license = lockbox.encrypt_io(license)
426
469
  ```
427
470
 
428
- Encrypt files before passing them to Shrine
471
+ To serve encrypted files, use a controller action.
429
472
 
430
473
  ```ruby
431
- LicenseUploader.upload(lockbox.encrypt_io(file), :store)
474
+ def license
475
+ user = User.find(params[:id])
476
+ lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
477
+ send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
478
+ end
432
479
  ```
433
480
 
434
- And decrypt them after reading
481
+ #### Non-Models
482
+
483
+ Generate a key
435
484
 
436
485
  ```ruby
437
- lockbox.decrypt(uploaded_file.read)
486
+ key = Lockbox.generate_key
438
487
  ```
439
488
 
440
- For models, encrypt with:
489
+ Create a lockbox
441
490
 
442
491
  ```ruby
443
- license = params.require(:user).fetch(:license)
444
- user.license = lockbox.encrypt_io(license)
492
+ lockbox = Lockbox.new(key: key)
445
493
  ```
446
494
 
447
- To serve encrypted files, use a controller action.
495
+ Encrypt files before passing them to Shrine
448
496
 
449
497
  ```ruby
450
- def license
451
- user = User.find(params[:id])
452
- send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
453
- end
498
+ LicenseUploader.upload(lockbox.encrypt_io(file), :store)
499
+ ```
500
+
501
+ And decrypt them after reading
502
+
503
+ ```ruby
504
+ lockbox.decrypt(uploaded_file.read)
454
505
  ```
455
506
 
456
507
  ## Local Files
@@ -655,6 +706,8 @@ This is the default algorithm. It’s:
655
706
  - an IETF standard
656
707
  - fast thanks to a [dedicated instruction set](https://en.wikipedia.org/wiki/AES_instruction_set)
657
708
 
709
+ Lockbox uses 256-bit keys.
710
+
658
711
  **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.
659
712
 
660
713
  ### XSalsa20
@@ -708,6 +761,22 @@ For Ubuntu 16.04, use:
708
761
  sudo apt-get install libsodium18
709
762
  ```
710
763
 
764
+ ##### GitHub Actions
765
+
766
+ For Ubuntu 20.04 and 18.04, use:
767
+
768
+ ```yml
769
+ - name: Install Libsodium
770
+ run: sudo apt-get update && sudo apt-get install libsodium23
771
+ ```
772
+
773
+ For Ubuntu 16.04, use:
774
+
775
+ ```yml
776
+ - name: Install Libsodium
777
+ run: sudo apt-get update && sudo apt-get install libsodium18
778
+ ```
779
+
711
780
  ##### Travis CI
712
781
 
713
782
  On Bionic, add to `.travis.yml`:
@@ -735,8 +804,7 @@ Add a step to `.circleci/config.yml`:
735
804
  ```yml
736
805
  - run:
737
806
  name: install Libsodium
738
- command: |
739
- sudo apt-get install -y libsodium18
807
+ command: sudo apt-get install -y libsodium18
740
808
  ```
741
809
 
742
810
  ## Hybrid Cryptography
@@ -773,7 +841,9 @@ Lockbox supports a few different ways to set keys for database fields and files.
773
841
 
774
842
  ### Master Key
775
843
 
776
- 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. You can get an individual key with:
844
+ 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.
845
+
846
+ You can get an individual key with:
777
847
 
778
848
  ```ruby
779
849
  Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
@@ -820,10 +890,14 @@ To use a different key for each record, use a symbol:
820
890
  ```ruby
821
891
  class User < ApplicationRecord
822
892
  encrypts :email, key: :some_method
893
+ end
894
+ ```
823
895
 
824
- def some_method
825
- # code to get key
826
- end
896
+ Or a proc:
897
+
898
+ ```ruby
899
+ class User < ApplicationRecord
900
+ encrypts :email, key: -> { some_method }
827
901
  end
828
902
  ```
829
903
 
@@ -938,12 +1012,6 @@ class User < ApplicationRecord
938
1012
  end
939
1013
  ```
940
1014
 
941
- or set it globally:
942
-
943
- ```ruby
944
- Lockbox.default_options = {encode: false}
945
- ```
946
-
947
1015
  ## Compatibility
948
1016
 
949
1017
  It’s easy to read encrypted data in another language if needed.
@@ -1019,12 +1087,29 @@ end
1019
1087
 
1020
1088
  ## Upgrading
1021
1089
 
1090
+ ### 0.6.0
1091
+
1092
+ 0.6.0 adds `encrypted: true` to Active Storage metadata for new files. This field is informational, but if you prefer to add it to existing files, use:
1093
+
1094
+ ```ruby
1095
+ User.with_attached_license.find_each do |user|
1096
+ next unless user.license.attached?
1097
+
1098
+ metadata = user.license.metadata
1099
+ unless metadata["encrypted"]
1100
+ user.license.blob.update!(metadata: metadata.merge("encrypted" => true))
1101
+ end
1102
+ end
1103
+ ```
1104
+
1022
1105
  ### 0.3.6
1023
1106
 
1024
1107
  0.3.6 makes content type detection more reliable for Active Storage. You can check and update the content type of existing files with:
1025
1108
 
1026
1109
  ```ruby
1027
- User.find_each do |user|
1110
+ User.with_attached_license.find_each do |user|
1111
+ next unless user.license.attached?
1112
+
1028
1113
  license = user.license
1029
1114
  content_type = Marcel::MimeType.for(license.download, name: license.filename.to_s)
1030
1115
  if content_type != license.content_type
@@ -16,9 +16,7 @@ module Lockbox
16
16
  end
17
17
 
18
18
  def data_type
19
- # use connection_config instead of connection.adapter
20
- # so database connection isn't needed
21
- case ActiveRecord::Base.connection_config[:adapter].to_s
19
+ case adapter
22
20
  when /postg/i # postgres, postgis
23
21
  "jsonb"
24
22
  when /mysql/i
@@ -27,6 +25,16 @@ module Lockbox
27
25
  "text"
28
26
  end
29
27
  end
28
+
29
+ # use connection_config instead of connection.adapter
30
+ # so database connection isn't needed
31
+ def adapter
32
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1
33
+ ActiveRecord::Base.connection_db_config.adapter.to_s
34
+ else
35
+ ActiveRecord::Base.connection_config[:adapter].to_s
36
+ end
37
+ end
30
38
  end
31
39
  end
32
40
  end
data/lib/lockbox.rb CHANGED
@@ -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"
@@ -26,6 +27,11 @@ end
26
27
 
27
28
  if defined?(ActiveSupport.on_load)
28
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
+
29
35
  extend Lockbox::Model
30
36
  extend Lockbox::Model::Attached
31
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
data/lib/lockbox/box.rb CHANGED
@@ -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)
data/lib/lockbox/model.rb CHANGED
@@ -22,21 +22,25 @@ module Lockbox
22
22
  # end
23
23
 
24
24
  custom_type = options[:type].respond_to?(:serialize) && options[:type].respond_to?(:deserialize)
25
- raise ArgumentError, "Unknown type: #{options[:type]}" unless custom_type || [nil, :string, :boolean, :date, :datetime, :time, :integer, :float, :binary, :json, :hash, :array].include?(options[:type])
25
+ valid_types = [nil, :string, :boolean, :date, :datetime, :time, :integer, :float, :binary, :json, :hash, :array, :inet]
26
+ raise ArgumentError, "Unknown type: #{options[:type]}" unless custom_type || valid_types.include?(options[:type])
26
27
 
27
28
  activerecord = defined?(ActiveRecord::Base) && self < ActiveRecord::Base
28
29
  raise ArgumentError, "Type not supported yet with Mongoid" if options[:type] && !activerecord
29
30
 
30
- # TODO raise ArgumentError in 0.5.0
31
- warn "[lockbox] WARNING: No attributes specified" if attributes.empty?
31
+ raise ArgumentError, "No attributes specified" if attributes.empty?
32
32
 
33
33
  raise ArgumentError, "Cannot use key_attribute with multiple attributes" if options[:key_attribute] && attributes.size > 1
34
34
 
35
+ original_options = options.dup
36
+
35
37
  attributes.each do |name|
36
- # add default options
37
- encrypted_attribute = "#{name}_ciphertext"
38
+ # per attribute options
39
+ # TODO use a different name
40
+ options = original_options.dup
38
41
 
39
- options = options.dup
42
+ # add default options
43
+ encrypted_attribute = options.delete(:encrypted_attribute) || "#{name}_ciphertext"
40
44
 
41
45
  # migrating
42
46
  original_name = name.to_sym
@@ -52,6 +56,15 @@ module Lockbox
52
56
  decrypt_method_name = "decrypt_#{encrypted_attribute}"
53
57
 
54
58
  class_eval do
59
+ # Lockbox uses custom inspect
60
+ # but this could be useful for other gems
61
+ if activerecord && ActiveRecord::VERSION::MAJOR >= 6
62
+ # only add virtual attribute
63
+ # need to use regexp since strings do partial matching
64
+ # also, need to use += instead of <<
65
+ self.filter_attributes += [/\A#{Regexp.escape(options[:attribute])}\z/]
66
+ end
67
+
55
68
  @lockbox_attributes ||= {}
56
69
 
57
70
  if @lockbox_attributes.empty?
@@ -76,12 +89,42 @@ module Lockbox
76
89
  super(options)
77
90
  end
78
91
 
79
- # use same approach as devise
92
+ # maintain order
93
+ # replace ciphertext attributes w/ virtual attributes (filtered)
80
94
  def inspect
81
- inspection =
82
- serializable_hash.map do |k,v|
83
- "#{k}: #{respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : v.inspect}"
95
+ lockbox_attributes = {}
96
+ lockbox_encrypted_attributes = {}
97
+ self.class.lockbox_attributes.each do |_, lockbox_attribute|
98
+ lockbox_attributes[lockbox_attribute[:attribute]] = true
99
+ lockbox_encrypted_attributes[lockbox_attribute[:encrypted_attribute]] = lockbox_attribute[:attribute]
100
+ end
101
+
102
+ inspection = []
103
+ # use serializable_hash like Devise
104
+ values = serializable_hash
105
+ self.class.attribute_names.each do |k|
106
+ next if !has_attribute?(k) || lockbox_attributes[k]
107
+
108
+ # check for lockbox attribute
109
+ if lockbox_encrypted_attributes[k]
110
+ # check if ciphertext attribute nil to avoid loading attribute
111
+ v = send(k).nil? ? "nil" : "[FILTERED]"
112
+ k = lockbox_encrypted_attributes[k]
113
+ elsif values.key?(k)
114
+ v = respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : values[k].inspect
115
+
116
+ # fix for https://github.com/rails/rails/issues/40725
117
+ # TODO only apply to Active Record 6.0
118
+ if respond_to?(:inspection_filter, true) && v != "nil"
119
+ v = inspection_filter.filter_param(k, v)
120
+ end
121
+ else
122
+ next
84
123
  end
124
+
125
+ inspection << "#{k}: #{v}"
126
+ end
127
+
85
128
  "#<#{self.class} #{inspection.join(", ")}>"
86
129
  end
87
130
 
@@ -169,6 +212,7 @@ module Lockbox
169
212
  end
170
213
 
171
214
  raise "Duplicate encrypted attribute: #{original_name}" if lockbox_attributes[original_name]
215
+ raise "Multiple encrypted attributes use the same column: #{encrypted_attribute}" if lockbox_attributes.any? { |_, v| v[:encrypted_attribute] == encrypted_attribute }
172
216
  @lockbox_attributes[original_name] = options
173
217
 
174
218
  if activerecord
@@ -204,6 +248,18 @@ module Lockbox
204
248
  else
205
249
  attribute name, :string
206
250
  end
251
+ else
252
+ # hack for Active Record 6.1
253
+ # to set string type after serialize
254
+ # otherwise, type gets set to ActiveModel::Type::Value
255
+ # which always returns false for changed_in_place?
256
+ # earlier versions of Active Record take the previous code path
257
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
258
+ attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call
259
+ if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
260
+ attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
261
+ end
262
+ end
207
263
  end
208
264
 
209
265
  define_method("#{name}_was") do
@@ -343,7 +399,7 @@ module Lockbox
343
399
  table = activerecord ? table_name : collection_name.to_s
344
400
 
345
401
  unless message.nil?
346
- # TODO use attribute type class in 0.5.0
402
+ # TODO use attribute type class in 0.7.0
347
403
  case options[:type]
348
404
  when :boolean
349
405
  message = ActiveRecord::Type::Boolean.new.serialize(message)
@@ -371,6 +427,14 @@ module Lockbox
371
427
  message = ActiveRecord::Type::Float.new.serialize(message)
372
428
  # double precision, big endian
373
429
  message = [message].pack("G") unless message.nil?
430
+ when :inet
431
+ unless message.nil?
432
+ ip = message.is_a?(IPAddr) ? message : (IPAddr.new(message) rescue nil)
433
+ # same format as Postgres, with ipv4 padded to 16 bytes
434
+ # family, netmask, ip
435
+ # return nil for invalid IP like Active Record
436
+ message = ip ? [ip.ipv4? ? 0 : 1, ip.prefix, ip.hton].pack("CCa16") : nil
437
+ end
374
438
  when :string, :binary
375
439
  # do nothing
376
440
  # encrypt will convert to binary
@@ -398,7 +462,7 @@ module Lockbox
398
462
  end
399
463
 
400
464
  unless message.nil?
401
- # TODO use attribute type class in 0.5.0
465
+ # TODO use attribute type class in 0.7.0
402
466
  case options[:type]
403
467
  when :boolean
404
468
  message = message == "t"
@@ -417,6 +481,11 @@ module Lockbox
417
481
  when :binary
418
482
  # do nothing
419
483
  # decrypt returns binary string
484
+ when :inet
485
+ family, prefix, addr = message.unpack("CCa16")
486
+ len = family == 0 ? 4 : 16
487
+ message = IPAddr.new_ntoh(addr.first(len))
488
+ message.prefix = prefix
420
489
  else
421
490
  # use original name for serialized attributes
422
491
  type = (try(:attribute_types) || {})[original_name.to_s]
@@ -1,6 +1,11 @@
1
1
  module Lockbox
2
2
  class Railtie < Rails::Railtie
3
3
  initializer "lockbox" do |app|
4
+ if defined?(Rails.application.credentials)
5
+ # needs to work when lockbox key has a string value
6
+ Lockbox.master_key ||= Rails.application.credentials.try(:lockbox).try(:fetch, :master_key, nil)
7
+ end
8
+
4
9
  require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
5
10
 
6
11
  if defined?(ActiveStorage)
data/lib/lockbox/utils.rb CHANGED
@@ -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|
@@ -26,10 +27,22 @@ module Lockbox
26
27
  end
27
28
 
28
29
  if options[:previous_versions].is_a?(Array)
29
- 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)
30
33
  options[:previous_versions].each_with_index do |version, i|
31
- if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
32
- 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)
33
46
  end
34
47
  end
35
48
  end
@@ -46,7 +59,7 @@ module Lockbox
46
59
  key = [key].pack("H*")
47
60
  end
48
61
 
49
- 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
50
63
  raise Lockbox::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
51
64
 
52
65
  key
@@ -76,13 +89,11 @@ module Lockbox
76
89
  attachable = attachable.dup
77
90
  attachable[:io] = box.encrypt_io(io)
78
91
  else
79
- # TODO raise ArgumentError
80
- 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}"
81
93
  end
82
94
 
83
95
  # don't analyze encrypted data
84
- metadata = {"analyzed" => true}
85
- metadata["encrypted"] = true if options[:migrating]
96
+ metadata = {"analyzed" => true, "encrypted" => true}
86
97
  attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
87
98
  end
88
99
 
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.8"
2
+ VERSION = "0.6.2"
3
3
  end
metadata CHANGED
@@ -1,171 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lockbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 0.6.2
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-31 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: carrierwave
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: combustion
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '1.3'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '1.3'
55
- - !ruby/object:Gem::Dependency
56
- name: rails
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: minitest
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '5'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '5'
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: rbnacl
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '6'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '6'
111
- - !ruby/object:Gem::Dependency
112
- name: sqlite3
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: pg
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: mysql2
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- - !ruby/object:Gem::Dependency
154
- name: benchmark-ips
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
- description:
168
- email: andrew@chartkick.com
11
+ date: 2021-02-08 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: andrew@ankane.org
169
15
  executables: []
170
16
  extensions: []
171
17
  extra_rdoc_files: []
@@ -197,7 +43,7 @@ homepage: https://github.com/ankane/lockbox
197
43
  licenses:
198
44
  - MIT
199
45
  metadata: {}
200
- post_install_message:
46
+ post_install_message:
201
47
  rdoc_options: []
202
48
  require_paths:
203
49
  - lib
@@ -212,8 +58,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
58
  - !ruby/object:Gem::Version
213
59
  version: '0'
214
60
  requirements: []
215
- rubygems_version: 3.1.2
216
- signing_key:
61
+ rubygems_version: 3.2.3
62
+ signing_key:
217
63
  specification_version: 4
218
- summary: Modern encryption for Rails
64
+ summary: Modern encryption for Ruby and Rails
219
65
  test_files: []