lockbox 0.4.9 → 0.6.3

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: 5ad5a754772ecb9d5f0a480cab88a63de9ed2fcfd973eef25f286fcf13da7694
4
- data.tar.gz: d38646c9d1aedee2bf12419a24064fa5b01e4ef4cb619e8cb48903c343ec67b1
3
+ metadata.gz: fbcd16c017fd2282888d124ab9d82029b92405bdaca39034ab4e52ee5eb92010
4
+ data.tar.gz: a54398f0e0acbd1543be296b4ca0214a3b6ca120dc5d89a5f488ac160309ea6b
5
5
  SHA512:
6
- metadata.gz: 5f6e78e05cb6788ad8188314694846f70c438e3e43f48bdf1e8e6356ac94e64226a3790ebaab6369121d1083d551a7203281979731443cfdb1c611d52a617493
7
- data.tar.gz: 30fc406d323dda8abdc0d5138880dc32b862abdd479e623180cf99c2531b466ef5a3be10620c975d0f2aee4b10e17b413cdc1cf21e03aa49ef5c6f0a7757cd82
6
+ metadata.gz: c1487a899e37749915555456b59ddb23b75bdc95ce8d8598750e13873c1226de7b7183d3306cfef3e3190ec52275ad452c25ac7c1aab7585a05630c1d312268c
7
+ data.tar.gz: 4686137e29c935e5f7f141c8239332f89d14764a94a70ee6160f138aa29cc1ce496a0fe384965c0d77ba795700fb58a4ed86feabf5318e25434b86ee0df47153
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ## 0.6.3 (2021-03-30)
2
+
3
+ - Fixed empty arrays and hashes
4
+ - Fixed content type for CarrierWave 2.2.1
5
+
6
+ ## 0.6.2 (2021-02-08)
7
+
8
+ - Added `inet` type
9
+ - Fixed error when `lockbox` key in Rails credentials has a string value
10
+ - Fixed deprecation warning with Active Record 6.1
11
+
12
+ ## 0.6.1 (2020-12-03)
13
+
14
+ - Added integration with Rails credentials
15
+ - Fixed in place changes for Active Record 6.1
16
+ - Fixed error with `content_type` method for CarrierWave < 2
17
+
18
+ ## 0.6.0 (2020-12-03)
19
+
20
+ - Added `encrypted` flag to Active Storage metadata
21
+ - Added encrypted columns to `filter_attributes`
22
+ - Improved `inspect` method
23
+
24
+ ## 0.5.0 (2020-11-22)
25
+
26
+ - Improved error messages for hybrid cryptography
27
+ - Changed warning to error when no attributes specified
28
+ - Fixed issue with `pluck` when migrating
29
+ - Fixed error with `key_table` and `key_attribute` options with `previous_versions`
30
+
1
31
  ## 0.4.9 (2020-10-01)
2
32
 
3
33
  - Added `key_table` and `key_attribute` options to `previous_versions`
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.
@@ -64,7 +72,7 @@ Then follow the instructions below for the data you want to encrypt.
64
72
  Create a migration with:
65
73
 
66
74
  ```ruby
67
- class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
75
+ class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.1]
68
76
  def change
69
77
  add_column :users, :email_ciphertext, :text
70
78
  end
@@ -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:
@@ -211,7 +248,7 @@ User.decrypt_email_ciphertext(user.email_ciphertext)
211
248
  Create a migration with:
212
249
 
213
250
  ```ruby
214
- class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[6.0]
251
+ class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[6.1]
215
252
  def change
216
253
  add_column :action_text_rich_texts, :body_ciphertext, :text
217
254
  end
@@ -342,7 +379,7 @@ Encryption is applied to all versions after processing.
342
379
  You can mount the uploader [as normal](https://github.com/carrierwaveuploader/carrierwave#activerecord). With Active Record, this involves creating a migration:
343
380
 
344
381
  ```ruby
345
- class AddLicenseToUsers < ActiveRecord::Migration[6.0]
382
+ class AddLicenseToUsers < ActiveRecord::Migration[6.1]
346
383
  def change
347
384
  add_column :users, :license, :string
348
385
  end
@@ -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
@@ -517,12 +568,10 @@ Update your model:
517
568
 
518
569
  ```ruby
519
570
  class User < ApplicationRecord
520
- encrypts :email, previous_versions: [{key: previous_key}]
571
+ encrypts :email, previous_versions: [{master_key: previous_key}]
521
572
  end
522
573
  ```
523
574
 
524
- Use `master_key` instead of `key` if passing the master key.
525
-
526
575
  To rotate existing records, use:
527
576
 
528
577
  ```ruby
@@ -536,11 +585,9 @@ Once all records are rotated, you can remove `previous_versions` from the model.
536
585
  Update your initializer:
537
586
 
538
587
  ```ruby
539
- Lockbox.encrypts_action_text_body(previous_versions: [{key: previous_key}])
588
+ Lockbox.encrypts_action_text_body(previous_versions: [{master_key: previous_key}])
540
589
  ```
541
590
 
542
- Use `master_key` instead of `key` if passing the master key.
543
-
544
591
  To rotate existing records, use:
545
592
 
546
593
  ```ruby
@@ -555,12 +602,10 @@ Update your model:
555
602
 
556
603
  ```ruby
557
604
  class User < ApplicationRecord
558
- encrypts_attached :license, previous_versions: [{key: previous_key}]
605
+ encrypts_attached :license, previous_versions: [{master_key: previous_key}]
559
606
  end
560
607
  ```
561
608
 
562
- Use `master_key` instead of `key` if passing the master key.
563
-
564
609
  To rotate existing files, use:
565
610
 
566
611
  ```ruby
@@ -577,12 +622,10 @@ Update your model:
577
622
 
578
623
  ```ruby
579
624
  class LicenseUploader < CarrierWave::Uploader::Base
580
- encrypt previous_versions: [{key: previous_key}]
625
+ encrypt previous_versions: [{master_key: previous_key}]
581
626
  end
582
627
  ```
583
628
 
584
- Use `master_key` instead of `key` if passing the master key.
585
-
586
629
  To rotate existing files, use:
587
630
 
588
631
  ```ruby
@@ -655,7 +698,9 @@ This is the default algorithm. It’s:
655
698
  - an IETF standard
656
699
  - fast thanks to a [dedicated instruction set](https://en.wikipedia.org/wiki/AES_instruction_set)
657
700
 
658
- **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.
701
+ Lockbox uses 256-bit keys.
702
+
703
+ **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 authentication key. Each database field and file uploader use a different key (derived from the master key) to extend this window.
659
704
 
660
705
  ### XSalsa20
661
706
 
@@ -708,6 +753,22 @@ For Ubuntu 16.04, use:
708
753
  sudo apt-get install libsodium18
709
754
  ```
710
755
 
756
+ ##### GitHub Actions
757
+
758
+ For Ubuntu 20.04 and 18.04, use:
759
+
760
+ ```yml
761
+ - name: Install Libsodium
762
+ run: sudo apt-get update && sudo apt-get install libsodium23
763
+ ```
764
+
765
+ For Ubuntu 16.04, use:
766
+
767
+ ```yml
768
+ - name: Install Libsodium
769
+ run: sudo apt-get update && sudo apt-get install libsodium18
770
+ ```
771
+
711
772
  ##### Travis CI
712
773
 
713
774
  On Bionic, add to `.travis.yml`:
@@ -735,8 +796,7 @@ Add a step to `.circleci/config.yml`:
735
796
  ```yml
736
797
  - run:
737
798
  name: install Libsodium
738
- command: |
739
- sudo apt-get install -y libsodium18
799
+ command: sudo apt-get install -y libsodium18
740
800
  ```
741
801
 
742
802
  ## Hybrid Cryptography
@@ -929,7 +989,7 @@ lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
929
989
  You can use `binary` columns for the ciphertext instead of `text` columns.
930
990
 
931
991
  ```ruby
932
- class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
992
+ class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.1]
933
993
  def change
934
994
  add_column :users, :email_ciphertext, :binary
935
995
  end
@@ -974,7 +1034,7 @@ end
974
1034
  Create a migration with:
975
1035
 
976
1036
  ```ruby
977
- class MigrateToLockbox < ActiveRecord::Migration[6.0]
1037
+ class MigrateToLockbox < ActiveRecord::Migration[6.1]
978
1038
  def change
979
1039
  add_column :users, :name_ciphertext, :text
980
1040
  add_column :users, :email_ciphertext, :text
@@ -1007,7 +1067,7 @@ end
1007
1067
  Then remove the previous gem from your Gemfile and drop its columns.
1008
1068
 
1009
1069
  ```ruby
1010
- class RemovePreviousEncryptedColumns < ActiveRecord::Migration[6.0]
1070
+ class RemovePreviousEncryptedColumns < ActiveRecord::Migration[6.1]
1011
1071
  def change
1012
1072
  remove_column :users, :encrypted_name, :text
1013
1073
  remove_column :users, :encrypted_name_iv, :text
@@ -1019,12 +1079,29 @@ end
1019
1079
 
1020
1080
  ## Upgrading
1021
1081
 
1082
+ ### 0.6.0
1083
+
1084
+ 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:
1085
+
1086
+ ```ruby
1087
+ User.with_attached_license.find_each do |user|
1088
+ next unless user.license.attached?
1089
+
1090
+ metadata = user.license.metadata
1091
+ unless metadata["encrypted"]
1092
+ user.license.blob.update!(metadata: metadata.merge("encrypted" => true))
1093
+ end
1094
+ end
1095
+ ```
1096
+
1022
1097
  ### 0.3.6
1023
1098
 
1024
1099
  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
1100
 
1026
1101
  ```ruby
1027
- User.find_each do |user|
1102
+ User.with_attached_license.find_each do |user|
1103
+ next unless user.license.attached?
1104
+
1028
1105
  license = user.license
1029
1106
  content_type = Marcel::MimeType.for(license.download, name: license.filename.to_s)
1030
1107
  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
@@ -43,11 +43,10 @@ module Lockbox
43
43
  cipher.auth_data = associated_data || ""
44
44
 
45
45
  begin
46
- if ciphertext.to_s.empty?
47
- cipher.final
48
- else
49
- cipher.update(ciphertext) + cipher.final
50
- end
46
+ message = String.new
47
+ message << cipher.update(ciphertext) unless ciphertext.to_s.empty?
48
+ message << cipher.final
49
+ message
51
50
  rescue OpenSSL::Cipher::CipherError
52
51
  fail_decryption
53
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,17 @@ 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 Gem::Version.new(CarrierWave::VERSION) >= Gem::Version.new("2.2.1")
37
+ # based on CarrierWave::SanitizedFile#marcel_magic_content_type
38
+ Marcel::Magic.by_magic(read).try(:type) || "invalid/invalid"
39
+ elsif CarrierWave::VERSION.to_i >= 2
40
+ # based on CarrierWave::SanitizedFile#mime_magic_content_type
41
+ MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
42
+ else
43
+ # uses filename
44
+ super
45
+ end
38
46
  end
39
47
 
40
48
  # disable processing since already processed
@@ -97,4 +105,11 @@ module Lockbox
97
105
  end
98
106
  end
99
107
 
108
+ if CarrierWave::VERSION.to_i > 2
109
+ raise "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
110
+ elsif CarrierWave::VERSION.to_i < 1
111
+ # TODO raise error in 0.7.0
112
+ warn "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
113
+ end
114
+
100
115
  CarrierWave::Uploader::Base.extend(Lockbox::CarrierWaveExtensions)
data/lib/lockbox/model.rb CHANGED
@@ -22,13 +22,13 @@ 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
 
@@ -56,6 +56,15 @@ module Lockbox
56
56
  decrypt_method_name = "decrypt_#{encrypted_attribute}"
57
57
 
58
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
+
59
68
  @lockbox_attributes ||= {}
60
69
 
61
70
  if @lockbox_attributes.empty?
@@ -80,15 +89,40 @@ module Lockbox
80
89
  super(options)
81
90
  end
82
91
 
83
- # use same approach as devise
92
+ # maintain order
93
+ # replace ciphertext attributes w/ virtual attributes (filtered)
84
94
  def inspect
85
- inspection =
86
- serializable_hash.map do |k,v|
87
- "#{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
88
123
  end
89
124
 
90
- self.class.lockbox_attributes.map do |_, lockbox_attribute|
91
- inspection << "#{lockbox_attribute[:attribute]}: [FILTERED]" if has_attribute?(lockbox_attribute[:encrypted_attribute])
125
+ inspection << "#{k}: #{v}"
92
126
  end
93
127
 
94
128
  "#<#{self.class} #{inspection.join(", ")}>"
@@ -160,8 +194,11 @@ module Lockbox
160
194
  attributes_to_set.each do |k, v|
161
195
  if respond_to?(:write_attribute_without_type_cast, true)
162
196
  write_attribute_without_type_cast(k, v)
163
- else
197
+ elsif respond_to?(:raw_write_attribute, true)
164
198
  raw_write_attribute(k, v)
199
+ else
200
+ @attributes.write_cast_value(k, v)
201
+ clear_attribute_change(k)
165
202
  end
166
203
  end
167
204
 
@@ -214,6 +251,23 @@ module Lockbox
214
251
  else
215
252
  attribute name, :string
216
253
  end
254
+ else
255
+ # hack for Active Record 6.1
256
+ # to set string type after serialize
257
+ # otherwise, type gets set to ActiveModel::Type::Value
258
+ # which always returns false for changed_in_place?
259
+ # earlier versions of Active Record take the previous code path
260
+ if ActiveRecord::VERSION::STRING.to_f >= 7.0 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
261
+ attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call(nil)
262
+ if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
263
+ attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
264
+ end
265
+ elsif ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
266
+ attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call
267
+ if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
268
+ attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
269
+ end
270
+ end
217
271
  end
218
272
 
219
273
  define_method("#{name}_was") do
@@ -325,7 +379,11 @@ module Lockbox
325
379
  # check for this explicitly as a layer of safety
326
380
  if message.nil? || ((message == {} || message == []) && activerecord && @attributes[name.to_s].value_before_type_cast.nil?)
327
381
  ciphertext = send(encrypted_attribute)
328
- message = self.class.send(decrypt_method_name, ciphertext, context: self)
382
+
383
+ # keep original message for empty hashes and arrays
384
+ unless ciphertext.nil?
385
+ message = self.class.send(decrypt_method_name, ciphertext, context: self)
386
+ end
329
387
 
330
388
  if activerecord
331
389
  # set previous attribute so changes populate correctly
@@ -337,8 +395,13 @@ module Lockbox
337
395
  # decrypt method does type casting
338
396
  if respond_to?(:write_attribute_without_type_cast, true)
339
397
  write_attribute_without_type_cast(name.to_s, message) if !@attributes.frozen?
340
- else
398
+ elsif respond_to?(:raw_write_attribute, true)
341
399
  raw_write_attribute(name, message) if !@attributes.frozen?
400
+ else
401
+ if !@attributes.frozen?
402
+ @attributes.write_cast_value(name.to_s, message)
403
+ clear_attribute_change(name)
404
+ end
342
405
  end
343
406
  else
344
407
  instance_variable_set("@#{name}", message)
@@ -353,7 +416,7 @@ module Lockbox
353
416
  table = activerecord ? table_name : collection_name.to_s
354
417
 
355
418
  unless message.nil?
356
- # TODO use attribute type class in 0.5.0
419
+ # TODO use attribute type class in 0.7.0
357
420
  case options[:type]
358
421
  when :boolean
359
422
  message = ActiveRecord::Type::Boolean.new.serialize(message)
@@ -381,6 +444,14 @@ module Lockbox
381
444
  message = ActiveRecord::Type::Float.new.serialize(message)
382
445
  # double precision, big endian
383
446
  message = [message].pack("G") unless message.nil?
447
+ when :inet
448
+ unless message.nil?
449
+ ip = message.is_a?(IPAddr) ? message : (IPAddr.new(message) rescue nil)
450
+ # same format as Postgres, with ipv4 padded to 16 bytes
451
+ # family, netmask, ip
452
+ # return nil for invalid IP like Active Record
453
+ message = ip ? [ip.ipv4? ? 0 : 1, ip.prefix, ip.hton].pack("CCa16") : nil
454
+ end
384
455
  when :string, :binary
385
456
  # do nothing
386
457
  # encrypt will convert to binary
@@ -408,7 +479,7 @@ module Lockbox
408
479
  end
409
480
 
410
481
  unless message.nil?
411
- # TODO use attribute type class in 0.5.0
482
+ # TODO use attribute type class in 0.7.0
412
483
  case options[:type]
413
484
  when :boolean
414
485
  message = message == "t"
@@ -427,6 +498,11 @@ module Lockbox
427
498
  when :binary
428
499
  # do nothing
429
500
  # decrypt returns binary string
501
+ when :inet
502
+ family, prefix, addr = message.unpack("CCa16")
503
+ len = family == 0 ? 4 : 16
504
+ message = IPAddr.new_ntoh(addr.first(len))
505
+ message.prefix = prefix
430
506
  else
431
507
  # use original name for serialized attributes
432
508
  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)
@@ -14,7 +19,14 @@ module Lockbox
14
19
  ActiveStorage::Attached::Many.prepend(Lockbox::ActiveStorageExtensions::AttachedMany)
15
20
 
16
21
  # use load hooks when possible
17
- if ActiveStorage::VERSION::MAJOR >= 6
22
+ if ActiveStorage::VERSION::MAJOR >= 7
23
+ ActiveSupport.on_load(:active_storage_attachment) do
24
+ prepend Lockbox::ActiveStorageExtensions::Attachment
25
+ end
26
+ ActiveSupport.on_load(:active_storage_blob) do
27
+ prepend Lockbox::ActiveStorageExtensions::Blob
28
+ end
29
+ elsif ActiveStorage::VERSION::MAJOR >= 6
18
30
  ActiveSupport.on_load(:active_storage_attachment) do
19
31
  include Lockbox::ActiveStorageExtensions::Attachment
20
32
  end
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,9 +27,11 @@ 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]
34
+ if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && (version[:master_key] || version[:key_table] || version[:key_attribute])
32
35
  # could also use key_table and key_attribute from options
33
36
  # when specified, but keep simple for now
34
37
  # also, this change isn't backward compatible
@@ -56,7 +59,7 @@ module Lockbox
56
59
  key = [key].pack("H*")
57
60
  end
58
61
 
59
- 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
60
63
  raise Lockbox::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
61
64
 
62
65
  key
@@ -86,13 +89,11 @@ module Lockbox
86
89
  attachable = attachable.dup
87
90
  attachable[:io] = box.encrypt_io(io)
88
91
  else
89
- # TODO raise ArgumentError
90
- 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}"
91
93
  end
92
94
 
93
95
  # don't analyze encrypted data
94
- metadata = {"analyzed" => true}
95
- metadata["encrypted"] = true if options[:migrating]
96
+ metadata = {"analyzed" => true, "encrypted" => true}
96
97
  attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
97
98
  end
98
99
 
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.9"
2
+ VERSION = "0.6.3"
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.9
4
+ version: 0.6.3
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-10-02 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-03-31 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.0.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: []