lockbox 0.4.3 → 0.4.8

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: c072d4c6e5935ff9e176c0fc705c0e36228da4b1fc530a7eaf0249db57c71ddc
4
- data.tar.gz: a0d293cb80ea7050deeccd039b5e0be01d51c09577181c1d9bf9df8a520764ac
3
+ metadata.gz: a560c020c3adf21952f81767ffc9b5b4586784f62d748f484e7bacbd4076a64a
4
+ data.tar.gz: 59d05b405b4cd46da679ef4f03a53fae03cc78d7cdfe89bab13cd6981b76a4da
5
5
  SHA512:
6
- metadata.gz: a46270931d8c21b25090be9d1825259e872eae678ba1d1df6ca7898f0e678f81edc2f590a4338502631686be3a7b2911e736c58d9704354918b707dbaafdba41
7
- data.tar.gz: 7bd511b855d777da969ea03aa14d7ce336a9c96670f01ac3e20424bb6fe039d43f183e561a33acb5e320ac2b8230aae287d608c292c68c4a91b605dd2be0cdb9
6
+ metadata.gz: 8d6217f47cc9c38ad8cf3db11b2a3a2936950b97f91ea168c5f2e4f8a1d9a5916c832286f08156869fbecf89d05dfc9bd7c4ecade9b9b4384488c936a292a1a6
7
+ data.tar.gz: 3ddf36244c68b6b0bebad62801366d9827e6bee520717f1d544cfc6a18e798c644a158b68ac295fa87cef45ce5b922f37e89c5e39a5882ccb9fe512e725e778b
@@ -1,3 +1,28 @@
1
+ ## 0.4.8 (2020-08-30)
2
+
3
+ - Added `key_table` and `key_attribute` options
4
+ - Added warning when no attributes specified
5
+ - Fixed error when Active Support partially loaded
6
+
7
+ ## 0.4.7 (2020-08-18)
8
+
9
+ - Added `lockbox_options` method to encrypted CarrierWave uploaders
10
+ - Improved attribute loading when no decryption key specified
11
+
12
+ ## 0.4.6 (2020-07-02)
13
+
14
+ - Added support for `update_column` and `update_columns`
15
+
16
+ ## 0.4.5 (2020-06-26)
17
+
18
+ - Improved error message for non-string values
19
+ - Fixed error with migrating Action Text
20
+ - Fixed error with migrating serialized attributes
21
+
22
+ ## 0.4.4 (2020-06-23)
23
+
24
+ - Added support for `pluck`
25
+
1
26
  ## 0.4.3 (2020-05-26)
2
27
 
3
28
  - Improved error message for bad key length
data/README.md CHANGED
@@ -2,12 +2,10 @@
2
2
 
3
3
  :package: Modern encryption for 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
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.
10
-
11
9
  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
10
 
13
11
  [![Build Status](https://travis-ci.org/ankane/lockbox.svg?branch=master)](https://travis-ci.org/ankane/lockbox)
@@ -89,6 +87,16 @@ User.create!(email: "hi@example.org")
89
87
 
90
88
  If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
91
89
 
90
+ #### Multiple Fields
91
+
92
+ You can specify multiple fields in single line.
93
+
94
+ ```ruby
95
+ class User < ApplicationRecord
96
+ encrypts :email, :phone, :city
97
+ end
98
+ ```
99
+
92
100
  #### Types
93
101
 
94
102
  Fields are strings by default. Specify the type of a field with:
@@ -188,8 +196,18 @@ class User < ApplicationRecord
188
196
  end
189
197
  ```
190
198
 
199
+ #### Decryption
200
+
201
+ To decrypt data outside the model, use:
202
+
203
+ ```ruby
204
+ User.decrypt_email_ciphertext(user.email_ciphertext)
205
+ ```
206
+
191
207
  ## Action Text
192
208
 
209
+ **Note:** Action Text uses direct uploads for files, which cannot be encrypted with application-level encryption like Lockbox. This only encrypts the database field.
210
+
193
211
  Create a migration with:
194
212
 
195
213
  ```ruby
@@ -220,6 +238,10 @@ Lockbox.encrypts_action_text_body
220
238
 
221
239
  And drop the unencrypted column.
222
240
 
241
+ #### Options
242
+
243
+ You can pass any Lockbox options to the `encrypts_action_text_body` method.
244
+
223
245
  ## Mongoid
224
246
 
225
247
  Add to your model:
@@ -264,8 +286,9 @@ end
264
286
 
265
287
  There are a few limitations to be aware of:
266
288
 
267
- - Metadata like image width and height are not extracted when encrypted
268
- - Direct uploads cannot be encrypted
289
+ - Variants and previews aren’t supported when encrypted
290
+ - Metadata like image width and height aren’t extracted when encrypted
291
+ - Direct uploads can’t be encrypted with application-level encryption like Lockbox, but can use server-side encryption
269
292
 
270
293
  To serve encrypted files, use a controller action.
271
294
 
@@ -508,6 +531,24 @@ Lockbox.rotate(User, attributes: [:email])
508
531
 
509
532
  Once all records are rotated, you can remove `previous_versions` from the model.
510
533
 
534
+ ### Action Text
535
+
536
+ Update your initializer:
537
+
538
+ ```ruby
539
+ Lockbox.encrypts_action_text_body(previous_versions: [{key: previous_key}])
540
+ ```
541
+
542
+ Use `master_key` instead of `key` if passing the master key.
543
+
544
+ To rotate existing records, use:
545
+
546
+ ```ruby
547
+ Lockbox.rotate(ActionText::RichText, attributes: [:body])
548
+ ```
549
+
550
+ Once all records are rotated, you can remove `previous_versions` from the initializer.
551
+
511
552
  ### Active Storage
512
553
 
513
554
  Update your model:
@@ -550,6 +591,14 @@ User.find_each do |user|
550
591
  end
551
592
  ```
552
593
 
594
+ For multiple files, use:
595
+
596
+ ```ruby
597
+ User.find_each do |user|
598
+ user.licenses.map(&:rotate_encryption!)
599
+ end
600
+ ```
601
+
553
602
  Once all files are rotated, you can remove `previous_versions` from the model.
554
603
 
555
604
  ### Local Files & Strings
@@ -714,15 +763,41 @@ Make sure `decryption_key` is `nil` on servers that shouldn’t decrypt.
714
763
 
715
764
  This uses X25519 for key exchange and XSalsa20 for encryption.
716
765
 
717
- ## Key Separation
766
+ ## Key Configuration
767
+
768
+ Lockbox supports a few different ways to set keys for database fields and files.
769
+
770
+ 1. Master key
771
+ 2. Per field/uploader
772
+ 3. Per record
718
773
 
719
- 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:
774
+ ### Master Key
775
+
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:
720
777
 
721
778
  ```ruby
722
779
  Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
723
780
  ```
724
781
 
725
- And set it directly before renaming:
782
+ To rename a table with encrypted columns/uploaders, use:
783
+
784
+ ```ruby
785
+ class User < ApplicationRecord
786
+ encrypts :email, key_table: "original_table"
787
+ end
788
+ ```
789
+
790
+ To rename an encrypted column itself, use:
791
+
792
+ ```ruby
793
+ class User < ApplicationRecord
794
+ encrypts :email, key_attribute: "original_column"
795
+ end
796
+ ```
797
+
798
+ ### Per Field/Uploader
799
+
800
+ To set a key for an individual field/uploader, use a string:
726
801
 
727
802
  ```ruby
728
803
  class User < ApplicationRecord
@@ -730,16 +805,58 @@ class User < ApplicationRecord
730
805
  end
731
806
  ```
732
807
 
808
+ Or a proc:
809
+
810
+ ```ruby
811
+ class User < ApplicationRecord
812
+ encrypts :email, key: -> { code }
813
+ end
814
+ ```
815
+
816
+ ### Per Record
817
+
818
+ To use a different key for each record, use a symbol:
819
+
820
+ ```ruby
821
+ class User < ApplicationRecord
822
+ encrypts :email, key: :some_method
823
+
824
+ def some_method
825
+ # code to get key
826
+ end
827
+ end
828
+ ```
829
+
733
830
  ## Key Management
734
831
 
735
832
  You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
736
833
 
834
+ For Active Record and Mongoid, use:
835
+
737
836
  ```ruby
738
837
  class User < ApplicationRecord
739
838
  encrypts :email, key: :kms_key
740
839
  end
741
840
  ```
742
841
 
842
+ For Action Text, use:
843
+
844
+ ```ruby
845
+ ActiveSupport.on_load(:action_text_rich_text) do
846
+ ActionText::RichText.has_kms_key
847
+ end
848
+
849
+ Lockbox.encrypts_action_text_body(key: :kms_key)
850
+ ```
851
+
852
+ For Active Storage, use:
853
+
854
+ ```ruby
855
+ class User < ApplicationRecord
856
+ encrypts_attached :license, key: :kms_key
857
+ end
858
+ ```
859
+
743
860
  For CarrierWave, use:
744
861
 
745
862
  ```ruby
@@ -772,7 +889,7 @@ lockbox.encrypt("clear").bytesize # 44
772
889
  lockbox.encrypt("consider").bytesize # 44
773
890
  ```
774
891
 
775
- The block size for padding is 16 bytes by default. If we have a status larger than 15 bytes, it will have a different length than the others.
892
+ The block size for padding is 16 bytes by default. Lockbox uses [ISO/IEC 7816-4](https://en.wikipedia.org/wiki/Padding_(cryptography)#ISO/IEC_7816-4) padding, which uses at least one byte, so if we have a status larger than 15 bytes, it will have a different length than the others.
776
893
 
777
894
  ```ruby
778
895
  box.encrypt("length15status!").bytesize # 44
@@ -785,9 +902,25 @@ Change the block size with:
785
902
  Lockbox.new(padding: 32) # bytes
786
903
  ```
787
904
 
905
+ ## Associated Data
906
+
907
+ You can pass extra context during encryption to make sure encrypted data isn’t moved to a different context.
908
+
909
+ ```ruby
910
+ lockbox = Lockbox.new(key: key)
911
+ ciphertext = lockbox.encrypt(message, associated_data: "somecontext")
912
+ ```
913
+
914
+ Without the same context, decryption will fail.
915
+
916
+ ```ruby
917
+ lockbox.decrypt(ciphertext, associated_data: "somecontext") # success
918
+ lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
919
+ ```
920
+
788
921
  ## Binary Columns
789
922
 
790
- You can use `binary` columns for the ciphertext instead of `text` columns to save space.
923
+ You can use `binary` columns for the ciphertext instead of `text` columns.
791
924
 
792
925
  ```ruby
793
926
  class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
@@ -797,7 +930,7 @@ class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
797
930
  end
798
931
  ```
799
932
 
800
- You should disable Base64 encoding if you do this.
933
+ Disable Base64 encoding to save space.
801
934
 
802
935
  ```ruby
803
936
  class User < ApplicationRecord
@@ -965,3 +1098,5 @@ cd lockbox
965
1098
  bundle install
966
1099
  bundle exec rake test
967
1100
  ```
1101
+
1102
+ For security issues, send an email to the address on [this page](https://github.com/ankane).
@@ -0,0 +1,3 @@
1
+ # Security Policy
2
+
3
+ For security issues, send an email to the address on [this page](https://github.com/ankane).
@@ -5,6 +5,7 @@ require "securerandom"
5
5
 
6
6
  # modules
7
7
  require "lockbox/box"
8
+ require "lockbox/calculations"
8
9
  require "lockbox/encryptor"
9
10
  require "lockbox/key_generator"
10
11
  require "lockbox/io"
@@ -18,13 +19,16 @@ require "lockbox/version"
18
19
  require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
19
20
  require "lockbox/railtie" if defined?(Rails)
20
21
 
21
- if defined?(ActiveSupport)
22
+ if defined?(ActiveSupport::LogSubscriber)
22
23
  require "lockbox/log_subscriber"
23
24
  Lockbox::LogSubscriber.attach_to :lockbox
25
+ end
24
26
 
27
+ if defined?(ActiveSupport.on_load)
25
28
  ActiveSupport.on_load(:active_record) do
26
29
  extend Lockbox::Model
27
30
  extend Lockbox::Model::Attached
31
+ ActiveRecord::Calculations.prepend Lockbox::Calculations
28
32
  end
29
33
 
30
34
  ActiveSupport.on_load(:mongoid) do
@@ -1,7 +1,22 @@
1
- # ideally encrypt and decrypt would happen at the blob/service level
2
- # however, there isn't really a great place to define encryption settings there
3
- # instead, we encrypt and decrypt at the attachment level,
4
- # and we define encryption settings at the model level
1
+ # Ideally encryption and decryption would happen at the blob/service level.
2
+ # However, Active Storage < 6.1 only supports a single service (per environment).
3
+ # This means all attachments need to be encrypted or none of them,
4
+ # which is often not practical.
5
+ #
6
+ # Active Storage 6.1 adds support for multiple services, which changes this.
7
+ # We could have a Lockbox service:
8
+ #
9
+ # lockbox:
10
+ # service: Lockbox
11
+ # backend: local # delegate to another service, like mirror service
12
+ # key: ... # Lockbox options
13
+ #
14
+ # However, the checksum is computed *and stored on the blob*
15
+ # before the file is passed to the service.
16
+ # We don't want the MD5 checksum of the plaintext stored in the database.
17
+ #
18
+ # Instead, we encrypt and decrypt at the attachment level,
19
+ # and we define encryption settings at the model level.
5
20
  module Lockbox
6
21
  module ActiveStorageExtensions
7
22
  module Attached
@@ -95,6 +110,16 @@ module Lockbox
95
110
  result
96
111
  end
97
112
 
113
+ def variant(*args)
114
+ raise Lockbox::Error, "Variant not supported for encrypted files" if Utils.encrypted_options(record, name)
115
+ super
116
+ end
117
+
118
+ def preview(*args)
119
+ raise Lockbox::Error, "Preview not supported for encrypted files" if Utils.encrypted_options(record, name)
120
+ super
121
+ end
122
+
98
123
  if ActiveStorage::VERSION::MAJOR >= 6
99
124
  def open(**options)
100
125
  blob.open(**options) do |file|
@@ -0,0 +1,36 @@
1
+ module Lockbox
2
+ module Calculations
3
+ def pluck(*column_names)
4
+ return super unless model.respond_to?(:lockbox_attributes)
5
+
6
+ lockbox_columns = column_names.map.with_index { |c, i| [model.lockbox_attributes[c.to_sym], i] }.select(&:first)
7
+ return super unless lockbox_columns.any?
8
+
9
+ # replace column with ciphertext column
10
+ lockbox_columns.each do |la, i|
11
+ column_names[i] = la[:encrypted_attribute]
12
+ end
13
+
14
+ # pluck
15
+ result = super(*column_names)
16
+
17
+ # decrypt result
18
+ # handle pluck to single columns and multiple
19
+ #
20
+ # we can't pass context to decrypt method
21
+ # so this won't work if any options are a symbol or proc
22
+ if column_names.size == 1
23
+ la = lockbox_columns.first.first
24
+ result.map! { |v| model.send("decrypt_#{la[:encrypted_attribute]}", v) }
25
+ else
26
+ lockbox_columns.each do |la, i|
27
+ result.each do |v|
28
+ v[i] = model.send("decrypt_#{la[:encrypted_attribute]}", v[i])
29
+ end
30
+ end
31
+ end
32
+
33
+ result
34
+ end
35
+ end
36
+ end
@@ -2,18 +2,32 @@ module Lockbox
2
2
  module CarrierWaveExtensions
3
3
  def encrypt(**options)
4
4
  class_eval do
5
+ # uses same hook as process (before cache)
6
+ # processing can be disabled, so better to keep separate
5
7
  before :cache, :encrypt
6
8
 
9
+ define_singleton_method :lockbox_options do
10
+ options
11
+ end
12
+
7
13
  def encrypt(file)
8
- @file = CarrierWave::SanitizedFile.new(with_notification("encrypt_file") { lockbox.encrypt_io(file) })
14
+ # safety check
15
+ # see CarrierWave::Uploader::Cache#cache!
16
+ raise Lockbox::Error, "Expected files to be equal. Please report an issue." unless file && @file && file == @file
17
+
18
+ # processors in CarrierWave move updated file to current_path
19
+ # however, this causes versions to use the processed file
20
+ # we only want to change the file for the current version
21
+ @file = CarrierWave::SanitizedFile.new(lockbox_notify("encrypt_file") { lockbox.encrypt_io(file) })
9
22
  end
10
23
 
11
24
  # TODO safe to memoize?
12
25
  def read
13
26
  r = super
14
- with_notification("decrypt_file") { lockbox.decrypt(r) } if r
27
+ lockbox_notify("decrypt_file") { lockbox.decrypt(r) } if r
15
28
  end
16
29
 
30
+ # use size of plaintext since read and content type use plaintext
17
31
  def size
18
32
  read.bytesize
19
33
  end
@@ -23,6 +37,7 @@ module Lockbox
23
37
  MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
24
38
  end
25
39
 
40
+ # disable processing since already processed
26
41
  def rotate_encryption!
27
42
  io = Lockbox::IO.new(read)
28
43
  io.original_filename = file.filename
@@ -46,6 +61,8 @@ module Lockbox
46
61
  end
47
62
  end
48
63
 
64
+ # for mounted uploaders, use mounted name
65
+ # for others, use uploader name
49
66
  def lockbox_name
50
67
  if mounted_as
51
68
  mounted_as.to_s
@@ -58,7 +75,9 @@ module Lockbox
58
75
  end
59
76
  end
60
77
 
61
- def with_notification(type)
78
+ # Active Support notifications so it's easier
79
+ # to see when files are encrypted and decrypted
80
+ def lockbox_notify(type)
62
81
  if defined?(ActiveSupport::Notifications)
63
82
  name = lockbox_name
64
83
 
@@ -13,7 +13,7 @@ module Lockbox
13
13
  end
14
14
 
15
15
  def encrypt(message, **options)
16
- message = check_string(message, "message")
16
+ message = check_string(message)
17
17
  ciphertext = @boxes.first.encrypt(message, **options)
18
18
  ciphertext = Base64.strict_encode64(ciphertext) if @encode
19
19
  ciphertext
@@ -21,7 +21,7 @@ module Lockbox
21
21
 
22
22
  def decrypt(ciphertext, **options)
23
23
  ciphertext = Base64.decode64(ciphertext) if @encode
24
- ciphertext = check_string(ciphertext, "ciphertext")
24
+ ciphertext = check_string(ciphertext)
25
25
 
26
26
  # ensure binary
27
27
  if ciphertext.encoding != Encoding::BINARY
@@ -66,9 +66,10 @@ module Lockbox
66
66
 
67
67
  private
68
68
 
69
- def check_string(str, name)
69
+ def check_string(str)
70
70
  str = str.read if str.respond_to?(:read)
71
- raise TypeError, "can't convert #{name} to string" unless str.respond_to?(:to_str)
71
+ # Ruby uses "no implicit conversion of Object into String"
72
+ raise TypeError, "can't convert #{str.class.name} to String" unless str.respond_to?(:to_str)
72
73
  str.to_str
73
74
  end
74
75
 
@@ -116,6 +116,13 @@ module Lockbox
116
116
  end
117
117
  end
118
118
 
119
+ # there's a small chance for this process to read data,
120
+ # another process to update the data, and
121
+ # this process to write the now stale data
122
+ # this time window can be reduced with smaller batch sizes
123
+ # locking individual records could eliminate this
124
+ # one option is: relation.in_batches { |batch| batch.lock }
125
+ # which runs SELECT ... FOR UPDATE in Postgres
119
126
  def migrate_records(records, fields:, blind_indexes:, restart:, rotate:)
120
127
  # do computation outside of transaction
121
128
  # especially expensive blind index computation
@@ -27,6 +27,11 @@ 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
+ # TODO raise ArgumentError in 0.5.0
31
+ warn "[lockbox] WARNING: No attributes specified" if attributes.empty?
32
+
33
+ raise ArgumentError, "Cannot use key_attribute with multiple attributes" if options[:key_attribute] && attributes.size > 1
34
+
30
35
  attributes.each do |name|
31
36
  # add default options
32
37
  encrypted_attribute = "#{name}_ciphertext"
@@ -87,6 +92,9 @@ module Lockbox
87
92
  # essentially a no-op if already loaded
88
93
  # an exception is thrown if decryption fails
89
94
  self.class.lockbox_attributes.each do |_, lockbox_attribute|
95
+ # don't try to decrypt if no decryption key given
96
+ next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
97
+
90
98
  # it is possible that the encrypted attribute is not loaded, eg.
91
99
  # if the record was fetched partially (`User.select(:id).first`).
92
100
  # accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
@@ -107,6 +115,49 @@ module Lockbox
107
115
  end
108
116
  end
109
117
  end
118
+
119
+ def update_columns(attributes)
120
+ return super unless attributes.is_a?(Hash)
121
+
122
+ # transform keys like Active Record
123
+ attributes = attributes.transform_keys do |key|
124
+ n = key.to_s
125
+ self.class.attribute_aliases[n] || n
126
+ end
127
+
128
+ lockbox_attributes = self.class.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
129
+ return super unless lockbox_attributes.any?
130
+
131
+ attributes_to_set = {}
132
+
133
+ lockbox_attributes.each do |key, lockbox_attribute|
134
+ attribute = key.to_s
135
+ # check read only
136
+ verify_readonly_attribute(attribute)
137
+
138
+ message = attributes[attribute]
139
+ attributes.delete(attribute) unless lockbox_attribute[:migrating]
140
+ encrypted_attribute = lockbox_attribute[:encrypted_attribute]
141
+ ciphertext = self.class.send("generate_#{encrypted_attribute}", message, context: self)
142
+ attributes[encrypted_attribute] = ciphertext
143
+ attributes_to_set[attribute] = message
144
+ attributes_to_set[lockbox_attribute[:attribute]] = message if lockbox_attribute[:migrating]
145
+ end
146
+
147
+ result = super(attributes)
148
+
149
+ # same logic as Active Record
150
+ # (although this happens before saving)
151
+ attributes_to_set.each do |k, v|
152
+ if respond_to?(:write_attribute_without_type_cast, true)
153
+ write_attribute_without_type_cast(k, v)
154
+ else
155
+ raw_write_attribute(k, v)
156
+ end
157
+ end
158
+
159
+ result
160
+ end
110
161
  else
111
162
  def reload
112
163
  self.class.lockbox_attributes.each do |_, v|
@@ -146,6 +197,10 @@ module Lockbox
146
197
  # however, we can try to use the original type if its already defined
147
198
  if attributes_to_define_after_schema_loads.key?(original_name.to_s)
148
199
  attribute name, attributes_to_define_after_schema_loads[original_name.to_s].first
200
+ elsif options[:migrating]
201
+ # we use the original attribute for serialization in the encrypt and decrypt methods
202
+ # so we can use a generic value here
203
+ attribute name, ActiveRecord::Type::Value.new
149
204
  else
150
205
  attribute name, :string
151
206
  end
@@ -216,12 +271,13 @@ module Lockbox
216
271
  define_method("#{name}=") do |message|
217
272
  # decrypt first for dirty tracking
218
273
  # don't raise error if can't decrypt previous
219
- begin
220
- send(name)
221
- rescue Lockbox::DecryptionError
222
- # this is expected for hybrid cryptography
223
- warn "[lockbox] Decrypting previous value failed" unless options[:algorithm] == "hybrid"
224
- nil
274
+ # don't try to decrypt if no decryption key given
275
+ unless options[:algorithm] == "hybrid" && options[:decryption_key].nil?
276
+ begin
277
+ send(name)
278
+ rescue Lockbox::DecryptionError
279
+ warn "[lockbox] Decrypting previous value failed"
280
+ end
225
281
  end
226
282
 
227
283
  send("lockbox_direct_#{name}=", message)
@@ -270,7 +326,7 @@ module Lockbox
270
326
  # cache
271
327
  # decrypt method does type casting
272
328
  if respond_to?(:write_attribute_without_type_cast, true)
273
- write_attribute_without_type_cast(name, message) if !@attributes.frozen?
329
+ write_attribute_without_type_cast(name.to_s, message) if !@attributes.frozen?
274
330
  else
275
331
  raw_write_attribute(name, message) if !@attributes.frozen?
276
332
  end
@@ -319,7 +375,8 @@ module Lockbox
319
375
  # do nothing
320
376
  # encrypt will convert to binary
321
377
  else
322
- type = (try(:attribute_types) || {})[name.to_s]
378
+ # use original name for serialized attributes
379
+ type = (try(:attribute_types) || {})[original_name.to_s]
323
380
  message = type.serialize(message) if type
324
381
  end
325
382
  end
@@ -361,7 +418,8 @@ module Lockbox
361
418
  # do nothing
362
419
  # decrypt returns binary string
363
420
  else
364
- type = (try(:attribute_types) || {})[name.to_s]
421
+ # use original name for serialized attributes
422
+ type = (try(:attribute_types) || {})[original_name.to_s]
365
423
  message = type.deserialize(message) if type
366
424
  message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
367
425
  end
@@ -4,22 +4,32 @@ module Lockbox
4
4
  options = options.except(:attribute, :encrypted_attribute, :migrating, :attached, :type)
5
5
  options[:encode] = false unless options.key?(:encode)
6
6
  options.each do |k, v|
7
- if v.is_a?(Proc)
8
- options[k] = context.instance_exec(&v) if v.respond_to?(:call)
7
+ if v.respond_to?(:call)
8
+ # context not present for pluck
9
+ # still possible to use if not dependent on context
10
+ options[k] = context ? context.instance_exec(&v) : v.call
9
11
  elsif v.is_a?(Symbol)
12
+ # context not present for pluck
13
+ raise Error, "Not available since :#{k} depends on record" unless context
10
14
  options[k] = context.send(v)
11
15
  end
12
16
  end
13
17
 
14
18
  unless options[:key] || options[:encryption_key] || options[:decryption_key]
15
- options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key))
19
+ options[:key] =
20
+ Lockbox.attribute_key(
21
+ table: options.delete(:key_table) || table,
22
+ attribute: options.delete(:key_attribute) || attribute,
23
+ master_key: options.delete(:master_key),
24
+ encode: false
25
+ )
16
26
  end
17
27
 
18
28
  if options[:previous_versions].is_a?(Array)
19
29
  options[:previous_versions] = options[:previous_versions].dup
20
30
  options[:previous_versions].each_with_index do |version, i|
21
31
  if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
22
- options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key)))
32
+ options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key), encode: false))
23
33
  end
24
34
  end
25
35
  end
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.3"
2
+ VERSION = "0.4.8"
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.3
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-26 00:00:00.000000000 Z
11
+ date: 2020-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -173,6 +173,7 @@ files:
173
173
  - CHANGELOG.md
174
174
  - LICENSE.txt
175
175
  - README.md
176
+ - SECURITY.md
176
177
  - lib/generators/lockbox/audits_generator.rb
177
178
  - lib/generators/lockbox/templates/migration.rb.tt
178
179
  - lib/generators/lockbox/templates/model.rb.tt
@@ -180,6 +181,7 @@ files:
180
181
  - lib/lockbox/active_storage_extensions.rb
181
182
  - lib/lockbox/aes_gcm.rb
182
183
  - lib/lockbox/box.rb
184
+ - lib/lockbox/calculations.rb
183
185
  - lib/lockbox/carrier_wave_extensions.rb
184
186
  - lib/lockbox/encryptor.rb
185
187
  - lib/lockbox/io.rb