lockbox 0.4.4 → 0.4.9

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: 6b1676e34ca9367ec4cfd3c655d263d23082243196abe8f0a174318383f7d4b1
4
- data.tar.gz: cdda6ac4e9dccc4ab48a67e3628212fdde369a99639cc93e569d0d623f5ab8f3
3
+ metadata.gz: 5ad5a754772ecb9d5f0a480cab88a63de9ed2fcfd973eef25f286fcf13da7694
4
+ data.tar.gz: d38646c9d1aedee2bf12419a24064fa5b01e4ef4cb619e8cb48903c343ec67b1
5
5
  SHA512:
6
- metadata.gz: 9e747bb720312daedcb2db5458d919cb50e554b03ee04d219a3b1c2f40ca917815a066dfd767b075046f29e387d5d48a734c193a3a2c48927b5abf6e0daaa099
7
- data.tar.gz: 2a456f05d61ecc75dfdc76aca15df194b58f91d701a28d12d3b27cc40f93f301244e4f60d506f7deae7a1b6a53afab087205aaced37a54af75f81810934ed8af
6
+ metadata.gz: 5f6e78e05cb6788ad8188314694846f70c438e3e43f48bdf1e8e6356ac94e64226a3790ebaab6369121d1083d551a7203281979731443cfdb1c611d52a617493
7
+ data.tar.gz: 30fc406d323dda8abdc0d5138880dc32b862abdd479e623180cf99c2531b466ef5a3be10620c975d0f2aee4b10e17b413cdc1cf21e03aa49ef5c6f0a7757cd82
@@ -1,3 +1,31 @@
1
+ ## 0.4.9 (2020-10-01)
2
+
3
+ - Added `key_table` and `key_attribute` options to `previous_versions`
4
+ - Added `encrypted_attribute` option
5
+ - Added support for encrypting empty string
6
+ - Improved `inspect` for models with encrypted attributes
7
+
8
+ ## 0.4.8 (2020-08-30)
9
+
10
+ - Added `key_table` and `key_attribute` options
11
+ - Added warning when no attributes specified
12
+ - Fixed error when Active Support partially loaded
13
+
14
+ ## 0.4.7 (2020-08-18)
15
+
16
+ - Added `lockbox_options` method to encrypted CarrierWave uploaders
17
+ - Improved attribute loading when no decryption key specified
18
+
19
+ ## 0.4.6 (2020-07-02)
20
+
21
+ - Added support for `update_column` and `update_columns`
22
+
23
+ ## 0.4.5 (2020-06-26)
24
+
25
+ - Improved error message for non-string values
26
+ - Fixed error with migrating Action Text
27
+ - Fixed error with migrating serialized attributes
28
+
1
29
  ## 0.4.4 (2020-06-23)
2
30
 
3
31
  - Added support for `pluck`
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,43 @@ 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.
777
+
778
+ You can get an individual key with:
720
779
 
721
780
  ```ruby
722
781
  Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
723
782
  ```
724
783
 
725
- And set it directly before renaming:
784
+ To rename a table with encrypted columns/uploaders, use:
785
+
786
+ ```ruby
787
+ class User < ApplicationRecord
788
+ encrypts :email, key_table: "original_table"
789
+ end
790
+ ```
791
+
792
+ To rename an encrypted column itself, use:
793
+
794
+ ```ruby
795
+ class User < ApplicationRecord
796
+ encrypts :email, key_attribute: "original_column"
797
+ end
798
+ ```
799
+
800
+ ### Per Field/Uploader
801
+
802
+ To set a key for an individual field/uploader, use a string:
726
803
 
727
804
  ```ruby
728
805
  class User < ApplicationRecord
@@ -730,16 +807,62 @@ class User < ApplicationRecord
730
807
  end
731
808
  ```
732
809
 
810
+ Or a proc:
811
+
812
+ ```ruby
813
+ class User < ApplicationRecord
814
+ encrypts :email, key: -> { code }
815
+ end
816
+ ```
817
+
818
+ ### Per Record
819
+
820
+ To use a different key for each record, use a symbol:
821
+
822
+ ```ruby
823
+ class User < ApplicationRecord
824
+ encrypts :email, key: :some_method
825
+ end
826
+ ```
827
+
828
+ Or a proc:
829
+
830
+ ```ruby
831
+ class User < ApplicationRecord
832
+ encrypts :email, key: -> { some_method }
833
+ end
834
+ ```
835
+
733
836
  ## Key Management
734
837
 
735
838
  You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
736
839
 
840
+ For Active Record and Mongoid, use:
841
+
737
842
  ```ruby
738
843
  class User < ApplicationRecord
739
844
  encrypts :email, key: :kms_key
740
845
  end
741
846
  ```
742
847
 
848
+ For Action Text, use:
849
+
850
+ ```ruby
851
+ ActiveSupport.on_load(:action_text_rich_text) do
852
+ ActionText::RichText.has_kms_key
853
+ end
854
+
855
+ Lockbox.encrypts_action_text_body(key: :kms_key)
856
+ ```
857
+
858
+ For Active Storage, use:
859
+
860
+ ```ruby
861
+ class User < ApplicationRecord
862
+ encrypts_attached :license, key: :kms_key
863
+ end
864
+ ```
865
+
743
866
  For CarrierWave, use:
744
867
 
745
868
  ```ruby
@@ -772,7 +895,7 @@ lockbox.encrypt("clear").bytesize # 44
772
895
  lockbox.encrypt("consider").bytesize # 44
773
896
  ```
774
897
 
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.
898
+ 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
899
 
777
900
  ```ruby
778
901
  box.encrypt("length15status!").bytesize # 44
@@ -785,9 +908,25 @@ Change the block size with:
785
908
  Lockbox.new(padding: 32) # bytes
786
909
  ```
787
910
 
911
+ ## Associated Data
912
+
913
+ You can pass extra context during encryption to make sure encrypted data isn’t moved to a different context.
914
+
915
+ ```ruby
916
+ lockbox = Lockbox.new(key: key)
917
+ ciphertext = lockbox.encrypt(message, associated_data: "somecontext")
918
+ ```
919
+
920
+ Without the same context, decryption will fail.
921
+
922
+ ```ruby
923
+ lockbox.decrypt(ciphertext, associated_data: "somecontext") # success
924
+ lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
925
+ ```
926
+
788
927
  ## Binary Columns
789
928
 
790
- You can use `binary` columns for the ciphertext instead of `text` columns to save space.
929
+ You can use `binary` columns for the ciphertext instead of `text` columns.
791
930
 
792
931
  ```ruby
793
932
  class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
@@ -797,7 +936,7 @@ class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
797
936
  end
798
937
  ```
799
938
 
800
- You should disable Base64 encoding if you do this.
939
+ Disable Base64 encoding to save space.
801
940
 
802
941
  ```ruby
803
942
  class User < ApplicationRecord
@@ -805,12 +944,6 @@ class User < ApplicationRecord
805
944
  end
806
945
  ```
807
946
 
808
- or set it globally:
809
-
810
- ```ruby
811
- Lockbox.default_options = {encode: false}
812
- ```
813
-
814
947
  ## Compatibility
815
948
 
816
949
  It’s easy to read encrypted data in another language if needed.
@@ -19,10 +19,12 @@ require "lockbox/version"
19
19
  require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
20
20
  require "lockbox/railtie" if defined?(Rails)
21
21
 
22
- if defined?(ActiveSupport)
22
+ if defined?(ActiveSupport::LogSubscriber)
23
23
  require "lockbox/log_subscriber"
24
24
  Lockbox::LogSubscriber.attach_to :lockbox
25
+ end
25
26
 
27
+ if defined?(ActiveSupport.on_load)
26
28
  ActiveSupport.on_load(:active_record) do
27
29
  extend Lockbox::Model
28
30
  extend Lockbox::Model::Attached
@@ -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|
@@ -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,11 @@ module Lockbox
43
43
  cipher.auth_data = associated_data || ""
44
44
 
45
45
  begin
46
- cipher.update(ciphertext) + cipher.final
46
+ if ciphertext.to_s.empty?
47
+ cipher.final
48
+ else
49
+ cipher.update(ciphertext) + cipher.final
50
+ end
47
51
  rescue OpenSSL::Cipher::CipherError
48
52
  fail_decryption
49
53
  end
@@ -2,9 +2,22 @@ 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)
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
8
21
  @file = CarrierWave::SanitizedFile.new(lockbox_notify("encrypt_file") { lockbox.encrypt_io(file) })
9
22
  end
10
23
 
@@ -14,6 +27,7 @@ module Lockbox
14
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,6 +75,8 @@ module Lockbox
58
75
  end
59
76
  end
60
77
 
78
+ # Active Support notifications so it's easier
79
+ # to see when files are encrypted and decrypted
61
80
  def lockbox_notify(type)
62
81
  if defined?(ActiveSupport::Notifications)
63
82
  name = lockbox_name
@@ -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,11 +27,20 @@ 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
+
35
+ original_options = options.dup
36
+
30
37
  attributes.each do |name|
31
- # add default options
32
- encrypted_attribute = "#{name}_ciphertext"
38
+ # per attribute options
39
+ # TODO use a different name
40
+ options = original_options.dup
33
41
 
34
- options = options.dup
42
+ # add default options
43
+ encrypted_attribute = options.delete(:encrypted_attribute) || "#{name}_ciphertext"
35
44
 
36
45
  # migrating
37
46
  original_name = name.to_sym
@@ -77,6 +86,11 @@ module Lockbox
77
86
  serializable_hash.map do |k,v|
78
87
  "#{k}: #{respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : v.inspect}"
79
88
  end
89
+
90
+ self.class.lockbox_attributes.map do |_, lockbox_attribute|
91
+ inspection << "#{lockbox_attribute[:attribute]}: [FILTERED]" if has_attribute?(lockbox_attribute[:encrypted_attribute])
92
+ end
93
+
80
94
  "#<#{self.class} #{inspection.join(", ")}>"
81
95
  end
82
96
 
@@ -87,6 +101,9 @@ module Lockbox
87
101
  # essentially a no-op if already loaded
88
102
  # an exception is thrown if decryption fails
89
103
  self.class.lockbox_attributes.each do |_, lockbox_attribute|
104
+ # don't try to decrypt if no decryption key given
105
+ next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
106
+
90
107
  # it is possible that the encrypted attribute is not loaded, eg.
91
108
  # if the record was fetched partially (`User.select(:id).first`).
92
109
  # accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
@@ -107,6 +124,49 @@ module Lockbox
107
124
  end
108
125
  end
109
126
  end
127
+
128
+ def update_columns(attributes)
129
+ return super unless attributes.is_a?(Hash)
130
+
131
+ # transform keys like Active Record
132
+ attributes = attributes.transform_keys do |key|
133
+ n = key.to_s
134
+ self.class.attribute_aliases[n] || n
135
+ end
136
+
137
+ lockbox_attributes = self.class.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
138
+ return super unless lockbox_attributes.any?
139
+
140
+ attributes_to_set = {}
141
+
142
+ lockbox_attributes.each do |key, lockbox_attribute|
143
+ attribute = key.to_s
144
+ # check read only
145
+ verify_readonly_attribute(attribute)
146
+
147
+ message = attributes[attribute]
148
+ attributes.delete(attribute) unless lockbox_attribute[:migrating]
149
+ encrypted_attribute = lockbox_attribute[:encrypted_attribute]
150
+ ciphertext = self.class.send("generate_#{encrypted_attribute}", message, context: self)
151
+ attributes[encrypted_attribute] = ciphertext
152
+ attributes_to_set[attribute] = message
153
+ attributes_to_set[lockbox_attribute[:attribute]] = message if lockbox_attribute[:migrating]
154
+ end
155
+
156
+ result = super(attributes)
157
+
158
+ # same logic as Active Record
159
+ # (although this happens before saving)
160
+ attributes_to_set.each do |k, v|
161
+ if respond_to?(:write_attribute_without_type_cast, true)
162
+ write_attribute_without_type_cast(k, v)
163
+ else
164
+ raw_write_attribute(k, v)
165
+ end
166
+ end
167
+
168
+ result
169
+ end
110
170
  else
111
171
  def reload
112
172
  self.class.lockbox_attributes.each do |_, v|
@@ -118,6 +178,7 @@ module Lockbox
118
178
  end
119
179
 
120
180
  raise "Duplicate encrypted attribute: #{original_name}" if lockbox_attributes[original_name]
181
+ raise "Multiple encrypted attributes use the same column: #{encrypted_attribute}" if lockbox_attributes.any? { |_, v| v[:encrypted_attribute] == encrypted_attribute }
121
182
  @lockbox_attributes[original_name] = options
122
183
 
123
184
  if activerecord
@@ -146,6 +207,10 @@ module Lockbox
146
207
  # however, we can try to use the original type if its already defined
147
208
  if attributes_to_define_after_schema_loads.key?(original_name.to_s)
148
209
  attribute name, attributes_to_define_after_schema_loads[original_name.to_s].first
210
+ elsif options[:migrating]
211
+ # we use the original attribute for serialization in the encrypt and decrypt methods
212
+ # so we can use a generic value here
213
+ attribute name, ActiveRecord::Type::Value.new
149
214
  else
150
215
  attribute name, :string
151
216
  end
@@ -216,12 +281,13 @@ module Lockbox
216
281
  define_method("#{name}=") do |message|
217
282
  # decrypt first for dirty tracking
218
283
  # 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
284
+ # don't try to decrypt if no decryption key given
285
+ unless options[:algorithm] == "hybrid" && options[:decryption_key].nil?
286
+ begin
287
+ send(name)
288
+ rescue Lockbox::DecryptionError
289
+ warn "[lockbox] Decrypting previous value failed"
290
+ end
225
291
  end
226
292
 
227
293
  send("lockbox_direct_#{name}=", message)
@@ -319,7 +385,8 @@ module Lockbox
319
385
  # do nothing
320
386
  # encrypt will convert to binary
321
387
  else
322
- type = (try(:attribute_types) || {})[name.to_s]
388
+ # use original name for serialized attributes
389
+ type = (try(:attribute_types) || {})[original_name.to_s]
323
390
  message = type.serialize(message) if type
324
391
  end
325
392
  end
@@ -361,7 +428,8 @@ module Lockbox
361
428
  # do nothing
362
429
  # decrypt returns binary string
363
430
  else
364
- type = (try(:attribute_types) || {})[name.to_s]
431
+ # use original name for serialized attributes
432
+ type = (try(:attribute_types) || {})[original_name.to_s]
365
433
  message = type.deserialize(message) if type
366
434
  message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
367
435
  end
@@ -16,14 +16,30 @@ module Lockbox
16
16
  end
17
17
 
18
18
  unless options[:key] || options[:encryption_key] || options[:decryption_key]
19
- options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key))
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
+ )
20
26
  end
21
27
 
22
28
  if options[:previous_versions].is_a?(Array)
23
29
  options[:previous_versions] = options[:previous_versions].dup
24
30
  options[:previous_versions].each_with_index do |version, i|
25
31
  if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
26
- options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key)))
32
+ # could also use key_table and key_attribute from options
33
+ # when specified, but keep simple for now
34
+ # also, this change isn't backward compatible
35
+ key =
36
+ Lockbox.attribute_key(
37
+ table: version.delete(:key_table) || table,
38
+ attribute: version.delete(:key_attribute) || attribute,
39
+ master_key: version.delete(:master_key),
40
+ encode: false
41
+ )
42
+ options[:previous_versions][i] = version.merge(key: key)
27
43
  end
28
44
  end
29
45
  end
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.4"
2
+ VERSION = "0.4.9"
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.4
4
+ version: 0.4.9
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-06-24 00:00:00.000000000 Z
11
+ date: 2020-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler