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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +146 -11
- data/SECURITY.md +3 -0
- data/lib/lockbox.rb +5 -1
- data/lib/lockbox/active_storage_extensions.rb +29 -4
- data/lib/lockbox/calculations.rb +36 -0
- data/lib/lockbox/carrier_wave_extensions.rb +22 -3
- data/lib/lockbox/encryptor.rb +5 -4
- data/lib/lockbox/migrator.rb +7 -0
- data/lib/lockbox/model.rb +67 -9
- data/lib/lockbox/utils.rb +14 -4
- data/lib/lockbox/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a560c020c3adf21952f81767ffc9b5b4586784f62d748f484e7bacbd4076a64a
|
4
|
+
data.tar.gz: 59d05b405b4cd46da679ef4f03a53fae03cc78d7cdfe89bab13cd6981b76a4da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d6217f47cc9c38ad8cf3db11b2a3a2936950b97f91ea168c5f2e4f8a1d9a5916c832286f08156869fbecf89d05dfc9bd7c4ecade9b9b4384488c936a292a1a6
|
7
|
+
data.tar.gz: 3ddf36244c68b6b0bebad62801366d9827e6bee520717f1d544cfc6a18e798c644a158b68ac295fa87cef45ce5b922f37e89c5e39a5882ccb9fe512e725e778b
|
data/CHANGELOG.md
CHANGED
@@ -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
|
[](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,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
|
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. 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
|
-
|
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.
|
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
|
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
|
-
|
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).
|
data/SECURITY.md
ADDED
data/lib/lockbox.rb
CHANGED
@@ -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
|
-
#
|
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|
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
|
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,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
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/lockbox/utils.rb
CHANGED
@@ -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.
|
8
|
-
|
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] =
|
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
|
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.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-
|
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
|