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 +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
|
[![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
|
-
-
|
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
|