lockbox 0.4.4 → 0.4.9
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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +150 -17
- data/lib/lockbox.rb +3 -1
- data/lib/lockbox/active_storage_extensions.rb +29 -4
- data/lib/lockbox/aes_gcm.rb +8 -4
- data/lib/lockbox/carrier_wave_extensions.rb +19 -0
- data/lib/lockbox/encryptor.rb +5 -4
- data/lib/lockbox/migrator.rb +7 -0
- data/lib/lockbox/model.rb +79 -11
- data/lib/lockbox/utils.rb +18 -2
- data/lib/lockbox/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ad5a754772ecb9d5f0a480cab88a63de9ed2fcfd973eef25f286fcf13da7694
|
4
|
+
data.tar.gz: d38646c9d1aedee2bf12419a24064fa5b01e4ef4cb619e8cb48903c343ec67b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f6e78e05cb6788ad8188314694846f70c438e3e43f48bdf1e8e6356ac94e64226a3790ebaab6369121d1083d551a7203281979731443cfdb1c611d52a617493
|
7
|
+
data.tar.gz: 30fc406d323dda8abdc0d5138880dc32b862abdd479e623180cf99c2531b466ef5a3be10620c975d0f2aee4b10e17b413cdc1cf21e03aa49ef5c6f0a7757cd82
|
data/CHANGELOG.md
CHANGED
@@ -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
|
[](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
|
-
-
|
268
|
-
-
|
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
|
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
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
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.
|
data/lib/lockbox.rb
CHANGED
@@ -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
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
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|
|
data/lib/lockbox/aes_gcm.rb
CHANGED
@@ -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 =
|
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
|
-
|
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
|
data/lib/lockbox/encryptor.rb
CHANGED
@@ -13,7 +13,7 @@ module Lockbox
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def encrypt(message, **options)
|
16
|
-
message = check_string(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
|
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
|
69
|
+
def check_string(str)
|
70
70
|
str = str.read if str.respond_to?(:read)
|
71
|
-
|
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
|
|
data/lib/lockbox/migrator.rb
CHANGED
@@ -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
|
data/lib/lockbox/model.rb
CHANGED
@@ -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
|
-
#
|
32
|
-
|
38
|
+
# per attribute options
|
39
|
+
# TODO use a different name
|
40
|
+
options = original_options.dup
|
33
41
|
|
34
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/lockbox/utils.rb
CHANGED
@@ -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] =
|
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
|
-
|
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
|
data/lib/lockbox/version.rb
CHANGED
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
|
+
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-
|
11
|
+
date: 2020-10-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|