lockbox 0.4.7 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +168 -33
- data/lib/lockbox.rb +9 -1
- data/lib/lockbox/active_storage_extensions.rb +4 -0
- data/lib/lockbox/aes_gcm.rb +7 -4
- data/lib/lockbox/box.rb +3 -4
- data/lib/lockbox/calculations.rb +2 -1
- data/lib/lockbox/carrier_wave_extensions.rb +14 -2
- data/lib/lockbox/model.rb +69 -9
- data/lib/lockbox/railtie.rb +4 -0
- data/lib/lockbox/utils.rb +26 -9
- data/lib/lockbox/version.rb +1 -1
- metadata +36 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 723ecf0e8367d053e2e80afddfa954559901f1b8c44d3a7cdb3ad562b3f5135a
|
4
|
+
data.tar.gz: 6c512616214fa1fdca743539769e1d2bd5f91135337a904b89ba58273dc9dbc5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 440177ac7cbe84f4e20eeedffe3045060debab17f178631bcba0b631e2e4c656bce1c0977f3af9dc84ce06788cf370626fed9053d3a4503632bc3daa0b6ab43d
|
7
|
+
data.tar.gz: ab23683aa75ff078b88cdfb65f29564691f5785b49549fa1d9286baf11a9959f7d49f0a21a7c84d10dd7d181c8affd05ca739769cdbe68193e4fb211f9a932af
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,35 @@
|
|
1
|
+
## 0.6.1 (2020-12-03)
|
2
|
+
|
3
|
+
- Added integration with Rails credentials
|
4
|
+
- Fixed in place changes for Active Record 6.1
|
5
|
+
- Fixed error with `content_type` method for CarrierWave < 2
|
6
|
+
|
7
|
+
## 0.6.0 (2020-12-03)
|
8
|
+
|
9
|
+
- Added `encrypted` flag to Active Storage metadata
|
10
|
+
- Added encrypted columns to `filter_attributes`
|
11
|
+
- Improved `inspect` method
|
12
|
+
|
13
|
+
## 0.5.0 (2020-11-22)
|
14
|
+
|
15
|
+
- Improved error messages for hybrid cryptography
|
16
|
+
- Changed warning to error when no attributes specified
|
17
|
+
- Fixed issue with `pluck` when migrating
|
18
|
+
- Fixed error with `key_table` and `key_attribute` options with `previous_versions`
|
19
|
+
|
20
|
+
## 0.4.9 (2020-10-01)
|
21
|
+
|
22
|
+
- Added `key_table` and `key_attribute` options to `previous_versions`
|
23
|
+
- Added `encrypted_attribute` option
|
24
|
+
- Added support for encrypting empty string
|
25
|
+
- Improved `inspect` for models with encrypted attributes
|
26
|
+
|
27
|
+
## 0.4.8 (2020-08-30)
|
28
|
+
|
29
|
+
- Added `key_table` and `key_attribute` options
|
30
|
+
- Added warning when no attributes specified
|
31
|
+
- Fixed error when Active Support partially loaded
|
32
|
+
|
1
33
|
## 0.4.7 (2020-08-18)
|
2
34
|
|
3
35
|
- Added `lockbox_options` method to encrypted CarrierWave uploaders
|
data/README.md
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
# Lockbox
|
2
2
|
|
3
|
-
:package: Modern encryption for Rails
|
3
|
+
:package: Modern encryption for Ruby and 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
|
-
|
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.
|
8
|
+
- Has zero dependencies and many integrations
|
10
9
|
|
11
10
|
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
11
|
|
13
|
-
[![Build Status](https://
|
12
|
+
[![Build Status](https://github.com/ankane/lockbox/workflows/build/badge.svg?branch=master)](https://github.com/ankane/lockbox/actions)
|
14
13
|
|
15
14
|
## Installation
|
16
15
|
|
@@ -36,10 +35,17 @@ Set the following environment variable with your key (you can use this one in de
|
|
36
35
|
LOCKBOX_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
37
36
|
```
|
38
37
|
|
38
|
+
or add it to your credentials for each environment (`rails credentials:edit --environment <env>` for Rails 6+)
|
39
|
+
|
40
|
+
```yml
|
41
|
+
lockbox:
|
42
|
+
master_key: "0000000000000000000000000000000000000000000000000000000000000000"
|
43
|
+
```
|
44
|
+
|
39
45
|
or create `config/initializers/lockbox.rb` with something like
|
40
46
|
|
41
47
|
```ruby
|
42
|
-
Lockbox.master_key = Rails.application.credentials.
|
48
|
+
Lockbox.master_key = Rails.application.credentials.lockbox[:master_key]
|
43
49
|
```
|
44
50
|
|
45
51
|
Then follow the instructions below for the data you want to encrypt.
|
@@ -89,6 +95,16 @@ User.create!(email: "hi@example.org")
|
|
89
95
|
|
90
96
|
If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
|
91
97
|
|
98
|
+
#### Multiple Fields
|
99
|
+
|
100
|
+
You can specify multiple fields in single line.
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
class User < ApplicationRecord
|
104
|
+
encrypts :email, :phone, :city
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
92
108
|
#### Types
|
93
109
|
|
94
110
|
Fields are strings by default. Specify the type of a field with:
|
@@ -188,6 +204,42 @@ class User < ApplicationRecord
|
|
188
204
|
end
|
189
205
|
```
|
190
206
|
|
207
|
+
#### Model Changes
|
208
|
+
|
209
|
+
If tracking changes to model attributes, be sure to remove or redact encrypted attributes.
|
210
|
+
|
211
|
+
PaperTrail
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
class User < ApplicationRecord
|
215
|
+
# for an encrypted history (still tracks ciphertext changes)
|
216
|
+
has_paper_trail skip: [:email]
|
217
|
+
|
218
|
+
# for no history (add blind indexes as well)
|
219
|
+
has_paper_trail skip: [:email, :email_ciphertext]
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
Audited
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
class User < ApplicationRecord
|
227
|
+
# for an encrypted history (still tracks ciphertext changes)
|
228
|
+
audited except: [:email]
|
229
|
+
|
230
|
+
# for no history (add blind indexes as well)
|
231
|
+
audited except: [:email, :email_ciphertext]
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
#### Decryption
|
236
|
+
|
237
|
+
To decrypt data outside the model, use:
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
User.decrypt_email_ciphertext(user.email_ciphertext)
|
241
|
+
```
|
242
|
+
|
191
243
|
## Action Text
|
192
244
|
|
193
245
|
**Note:** Action Text uses direct uploads for files, which cannot be encrypted with application-level encryption like Lockbox. This only encrypts the database field.
|
@@ -222,6 +274,10 @@ Lockbox.encrypts_action_text_body
|
|
222
274
|
|
223
275
|
And drop the unencrypted column.
|
224
276
|
|
277
|
+
#### Options
|
278
|
+
|
279
|
+
You can pass any Lockbox options to the `encrypts_action_text_body` method.
|
280
|
+
|
225
281
|
## Mongoid
|
226
282
|
|
227
283
|
Add to your model:
|
@@ -393,44 +449,58 @@ Finally, delete the unencrypted files and drop the column for the original uploa
|
|
393
449
|
|
394
450
|
## Shrine
|
395
451
|
|
396
|
-
|
452
|
+
#### Models
|
453
|
+
|
454
|
+
Include the attachment as normal:
|
397
455
|
|
398
456
|
```ruby
|
399
|
-
|
457
|
+
class User < ApplicationRecord
|
458
|
+
include LicenseUploader::Attachment(:license)
|
459
|
+
end
|
400
460
|
```
|
401
461
|
|
402
|
-
|
462
|
+
And encrypt in a controller (or background job, etc) with:
|
403
463
|
|
404
464
|
```ruby
|
405
|
-
|
465
|
+
license = params.require(:user).fetch(:license)
|
466
|
+
lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
|
467
|
+
user.license = lockbox.encrypt_io(license)
|
406
468
|
```
|
407
469
|
|
408
|
-
|
470
|
+
To serve encrypted files, use a controller action.
|
409
471
|
|
410
472
|
```ruby
|
411
|
-
|
473
|
+
def license
|
474
|
+
user = User.find(params[:id])
|
475
|
+
lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
|
476
|
+
send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
|
477
|
+
end
|
412
478
|
```
|
413
479
|
|
414
|
-
|
480
|
+
#### Non-Models
|
481
|
+
|
482
|
+
Generate a key
|
415
483
|
|
416
484
|
```ruby
|
417
|
-
|
485
|
+
key = Lockbox.generate_key
|
418
486
|
```
|
419
487
|
|
420
|
-
|
488
|
+
Create a lockbox
|
421
489
|
|
422
490
|
```ruby
|
423
|
-
|
424
|
-
user.license = lockbox.encrypt_io(license)
|
491
|
+
lockbox = Lockbox.new(key: key)
|
425
492
|
```
|
426
493
|
|
427
|
-
|
494
|
+
Encrypt files before passing them to Shrine
|
428
495
|
|
429
496
|
```ruby
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
497
|
+
LicenseUploader.upload(lockbox.encrypt_io(file), :store)
|
498
|
+
```
|
499
|
+
|
500
|
+
And decrypt them after reading
|
501
|
+
|
502
|
+
```ruby
|
503
|
+
lockbox.decrypt(uploaded_file.read)
|
434
504
|
```
|
435
505
|
|
436
506
|
## Local Files
|
@@ -635,6 +705,8 @@ This is the default algorithm. It’s:
|
|
635
705
|
- an IETF standard
|
636
706
|
- fast thanks to a [dedicated instruction set](https://en.wikipedia.org/wiki/AES_instruction_set)
|
637
707
|
|
708
|
+
Lockbox uses 256-bit keys.
|
709
|
+
|
638
710
|
**For users who do a lot of encryptions:** You should rotate an individual key after 2 billion encryptions to minimize the chance of a [nonce collision](https://www.cryptologie.net/article/402/is-symmetric-security-solved/), which will expose the key. Each database field and file uploader use a different key (derived from the master key) to extend this window.
|
639
711
|
|
640
712
|
### XSalsa20
|
@@ -688,6 +760,22 @@ For Ubuntu 16.04, use:
|
|
688
760
|
sudo apt-get install libsodium18
|
689
761
|
```
|
690
762
|
|
763
|
+
##### GitHub Actions
|
764
|
+
|
765
|
+
For Ubuntu 20.04 and 18.04, use:
|
766
|
+
|
767
|
+
```yml
|
768
|
+
- name: Install Libsodium
|
769
|
+
run: sudo apt-get update && sudo apt-get install libsodium23
|
770
|
+
```
|
771
|
+
|
772
|
+
For Ubuntu 16.04, use:
|
773
|
+
|
774
|
+
```yml
|
775
|
+
- name: Install Libsodium
|
776
|
+
run: sudo apt-get update && sudo apt-get install libsodium18
|
777
|
+
```
|
778
|
+
|
691
779
|
##### Travis CI
|
692
780
|
|
693
781
|
On Bionic, add to `.travis.yml`:
|
@@ -715,8 +803,7 @@ Add a step to `.circleci/config.yml`:
|
|
715
803
|
```yml
|
716
804
|
- run:
|
717
805
|
name: install Libsodium
|
718
|
-
command:
|
719
|
-
sudo apt-get install -y libsodium18
|
806
|
+
command: sudo apt-get install -y libsodium18
|
720
807
|
```
|
721
808
|
|
722
809
|
## Hybrid Cryptography
|
@@ -743,15 +830,43 @@ Make sure `decryption_key` is `nil` on servers that shouldn’t decrypt.
|
|
743
830
|
|
744
831
|
This uses X25519 for key exchange and XSalsa20 for encryption.
|
745
832
|
|
746
|
-
## Key
|
833
|
+
## Key Configuration
|
834
|
+
|
835
|
+
Lockbox supports a few different ways to set keys for database fields and files.
|
836
|
+
|
837
|
+
1. Master key
|
838
|
+
2. Per field/uploader
|
839
|
+
3. Per record
|
747
840
|
|
748
|
-
|
841
|
+
### Master Key
|
842
|
+
|
843
|
+
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.
|
844
|
+
|
845
|
+
You can get an individual key with:
|
749
846
|
|
750
847
|
```ruby
|
751
848
|
Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
|
752
849
|
```
|
753
850
|
|
754
|
-
|
851
|
+
To rename a table with encrypted columns/uploaders, use:
|
852
|
+
|
853
|
+
```ruby
|
854
|
+
class User < ApplicationRecord
|
855
|
+
encrypts :email, key_table: "original_table"
|
856
|
+
end
|
857
|
+
```
|
858
|
+
|
859
|
+
To rename an encrypted column itself, use:
|
860
|
+
|
861
|
+
```ruby
|
862
|
+
class User < ApplicationRecord
|
863
|
+
encrypts :email, key_attribute: "original_column"
|
864
|
+
end
|
865
|
+
```
|
866
|
+
|
867
|
+
### Per Field/Uploader
|
868
|
+
|
869
|
+
To set a key for an individual field/uploader, use a string:
|
755
870
|
|
756
871
|
```ruby
|
757
872
|
class User < ApplicationRecord
|
@@ -759,6 +874,32 @@ class User < ApplicationRecord
|
|
759
874
|
end
|
760
875
|
```
|
761
876
|
|
877
|
+
Or a proc:
|
878
|
+
|
879
|
+
```ruby
|
880
|
+
class User < ApplicationRecord
|
881
|
+
encrypts :email, key: -> { code }
|
882
|
+
end
|
883
|
+
```
|
884
|
+
|
885
|
+
### Per Record
|
886
|
+
|
887
|
+
To use a different key for each record, use a symbol:
|
888
|
+
|
889
|
+
```ruby
|
890
|
+
class User < ApplicationRecord
|
891
|
+
encrypts :email, key: :some_method
|
892
|
+
end
|
893
|
+
```
|
894
|
+
|
895
|
+
Or a proc:
|
896
|
+
|
897
|
+
```ruby
|
898
|
+
class User < ApplicationRecord
|
899
|
+
encrypts :email, key: -> { some_method }
|
900
|
+
end
|
901
|
+
```
|
902
|
+
|
762
903
|
## Key Management
|
763
904
|
|
764
905
|
You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
|
@@ -870,12 +1011,6 @@ class User < ApplicationRecord
|
|
870
1011
|
end
|
871
1012
|
```
|
872
1013
|
|
873
|
-
or set it globally:
|
874
|
-
|
875
|
-
```ruby
|
876
|
-
Lockbox.default_options = {encode: false}
|
877
|
-
```
|
878
|
-
|
879
1014
|
## Compatibility
|
880
1015
|
|
881
1016
|
It’s easy to read encrypted data in another language if needed.
|
data/lib/lockbox.rb
CHANGED
@@ -4,6 +4,7 @@ require "openssl"
|
|
4
4
|
require "securerandom"
|
5
5
|
|
6
6
|
# modules
|
7
|
+
require "lockbox/aes_gcm"
|
7
8
|
require "lockbox/box"
|
8
9
|
require "lockbox/calculations"
|
9
10
|
require "lockbox/encryptor"
|
@@ -19,11 +20,18 @@ require "lockbox/version"
|
|
19
20
|
require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
|
20
21
|
require "lockbox/railtie" if defined?(Rails)
|
21
22
|
|
22
|
-
if defined?(ActiveSupport)
|
23
|
+
if defined?(ActiveSupport::LogSubscriber)
|
23
24
|
require "lockbox/log_subscriber"
|
24
25
|
Lockbox::LogSubscriber.attach_to :lockbox
|
26
|
+
end
|
25
27
|
|
28
|
+
if defined?(ActiveSupport.on_load)
|
26
29
|
ActiveSupport.on_load(:active_record) do
|
30
|
+
# TODO raise error in 0.7.0
|
31
|
+
if ActiveRecord::VERSION::STRING.to_f <= 5.0
|
32
|
+
warn "Active Record version (#{ActiveRecord::VERSION::STRING}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
|
33
|
+
end
|
34
|
+
|
27
35
|
extend Lockbox::Model
|
28
36
|
extend Lockbox::Model::Attached
|
29
37
|
ActiveRecord::Calculations.prepend Lockbox::Calculations
|
@@ -89,6 +89,10 @@ module Lockbox
|
|
89
89
|
module CreateOne
|
90
90
|
def initialize(name, record, attachable)
|
91
91
|
# this won't encrypt existing blobs
|
92
|
+
# ideally we'd check metadata for the encrypted flag
|
93
|
+
# and disallow unencrypted blobs
|
94
|
+
# since they'll raise an error on decryption
|
95
|
+
# but earlier versions of Lockbox won't have it
|
92
96
|
attachable = Lockbox::Utils.encrypt_attachable(record, name, attachable) if Lockbox::Utils.encrypted?(record, name) && !attachable.is_a?(ActiveStorage::Blob)
|
93
97
|
super(name, record, attachable)
|
94
98
|
end
|
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,10 @@ module Lockbox
|
|
43
43
|
cipher.auth_data = associated_data || ""
|
44
44
|
|
45
45
|
begin
|
46
|
-
|
46
|
+
message = String.new
|
47
|
+
message << cipher.update(ciphertext) unless ciphertext.to_s.empty?
|
48
|
+
message << cipher.final
|
49
|
+
message
|
47
50
|
rescue OpenSSL::Cipher::CipherError
|
48
51
|
fail_decryption
|
49
52
|
end
|
data/lib/lockbox/box.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Lockbox
|
2
2
|
class Box
|
3
3
|
def initialize(key: nil, algorithm: nil, encryption_key: nil, decryption_key: nil, padding: false)
|
4
|
-
raise ArgumentError, "Cannot pass both key and
|
4
|
+
raise ArgumentError, "Cannot pass both key and encryption/decryption key" if key && (encryption_key || decryption_key)
|
5
5
|
|
6
6
|
key = Lockbox::Utils.decode_key(key) if key
|
7
7
|
encryption_key = Lockbox::Utils.decode_key(encryption_key, size: 64) if encryption_key
|
@@ -12,7 +12,6 @@ module Lockbox
|
|
12
12
|
case algorithm
|
13
13
|
when "aes-gcm"
|
14
14
|
raise ArgumentError, "Missing key" unless key
|
15
|
-
require "lockbox/aes_gcm"
|
16
15
|
@box = AES_GCM.new(key)
|
17
16
|
when "xchacha20"
|
18
17
|
raise ArgumentError, "Missing key" unless key
|
@@ -39,7 +38,7 @@ module Lockbox
|
|
39
38
|
message = Lockbox.pad(message, size: @padding) if @padding
|
40
39
|
case @algorithm
|
41
40
|
when "hybrid"
|
42
|
-
raise ArgumentError, "No
|
41
|
+
raise ArgumentError, "No encryption key set" unless defined?(@encryption_box)
|
43
42
|
raise ArgumentError, "Associated data not supported with this algorithm" if associated_data
|
44
43
|
nonce = generate_nonce(@encryption_box)
|
45
44
|
ciphertext = @encryption_box.encrypt(nonce, message)
|
@@ -58,7 +57,7 @@ module Lockbox
|
|
58
57
|
message =
|
59
58
|
case @algorithm
|
60
59
|
when "hybrid"
|
61
|
-
raise ArgumentError, "No
|
60
|
+
raise ArgumentError, "No decryption key set" unless defined?(@decryption_box)
|
62
61
|
raise ArgumentError, "Associated data not supported with this algorithm" if associated_data
|
63
62
|
nonce, ciphertext = extract_nonce(@decryption_box, ciphertext)
|
64
63
|
@decryption_box.decrypt(nonce, ciphertext)
|
data/lib/lockbox/calculations.rb
CHANGED
@@ -3,7 +3,8 @@ module Lockbox
|
|
3
3
|
def pluck(*column_names)
|
4
4
|
return super unless model.respond_to?(:lockbox_attributes)
|
5
5
|
|
6
|
-
lockbox_columns = column_names.map.with_index { |c, i| [model.lockbox_attributes[c.to_sym], i] }.select
|
6
|
+
lockbox_columns = column_names.map.with_index { |c, i| [model.lockbox_attributes[c.to_sym], i] }.select { |la, _i| la && !la[:migrating] }
|
7
|
+
|
7
8
|
return super unless lockbox_columns.any?
|
8
9
|
|
9
10
|
# replace column with ciphertext column
|
@@ -32,9 +32,14 @@ module Lockbox
|
|
32
32
|
read.bytesize
|
33
33
|
end
|
34
34
|
|
35
|
-
# based on CarrierWave::SanitizedFile#mime_magic_content_type
|
36
35
|
def content_type
|
37
|
-
|
36
|
+
if CarrierWave::VERSION.to_i >= 2
|
37
|
+
# based on CarrierWave::SanitizedFile#mime_magic_content_type
|
38
|
+
MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
|
39
|
+
else
|
40
|
+
# uses filename
|
41
|
+
super
|
42
|
+
end
|
38
43
|
end
|
39
44
|
|
40
45
|
# disable processing since already processed
|
@@ -97,4 +102,11 @@ module Lockbox
|
|
97
102
|
end
|
98
103
|
end
|
99
104
|
|
105
|
+
if CarrierWave::VERSION.to_i > 2
|
106
|
+
raise "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
|
107
|
+
elsif CarrierWave::VERSION.to_i < 1
|
108
|
+
# TODO raise error in 0.7.0
|
109
|
+
warn "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
|
110
|
+
end
|
111
|
+
|
100
112
|
CarrierWave::Uploader::Base.extend(Lockbox::CarrierWaveExtensions)
|
data/lib/lockbox/model.rb
CHANGED
@@ -27,11 +27,19 @@ 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
|
+
raise ArgumentError, "No attributes specified" if attributes.empty?
|
31
|
+
|
32
|
+
raise ArgumentError, "Cannot use key_attribute with multiple attributes" if options[:key_attribute] && attributes.size > 1
|
33
|
+
|
34
|
+
original_options = options.dup
|
35
|
+
|
30
36
|
attributes.each do |name|
|
31
|
-
#
|
32
|
-
|
37
|
+
# per attribute options
|
38
|
+
# TODO use a different name
|
39
|
+
options = original_options.dup
|
33
40
|
|
34
|
-
|
41
|
+
# add default options
|
42
|
+
encrypted_attribute = options.delete(:encrypted_attribute) || "#{name}_ciphertext"
|
35
43
|
|
36
44
|
# migrating
|
37
45
|
original_name = name.to_sym
|
@@ -47,6 +55,15 @@ module Lockbox
|
|
47
55
|
decrypt_method_name = "decrypt_#{encrypted_attribute}"
|
48
56
|
|
49
57
|
class_eval do
|
58
|
+
# Lockbox uses custom inspect
|
59
|
+
# but this could be useful for other gems
|
60
|
+
if activerecord && ActiveRecord::VERSION::MAJOR >= 6
|
61
|
+
# only add virtual attribute
|
62
|
+
# need to use regexp since strings do partial matching
|
63
|
+
# also, need to use += instead of <<
|
64
|
+
self.filter_attributes += [/\A#{Regexp.escape(options[:attribute])}\z/]
|
65
|
+
end
|
66
|
+
|
50
67
|
@lockbox_attributes ||= {}
|
51
68
|
|
52
69
|
if @lockbox_attributes.empty?
|
@@ -71,12 +88,42 @@ module Lockbox
|
|
71
88
|
super(options)
|
72
89
|
end
|
73
90
|
|
74
|
-
#
|
91
|
+
# maintain order
|
92
|
+
# replace ciphertext attributes w/ virtual attributes (filtered)
|
75
93
|
def inspect
|
76
|
-
|
77
|
-
|
78
|
-
|
94
|
+
lockbox_attributes = {}
|
95
|
+
lockbox_encrypted_attributes = {}
|
96
|
+
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
97
|
+
lockbox_attributes[lockbox_attribute[:attribute]] = true
|
98
|
+
lockbox_encrypted_attributes[lockbox_attribute[:encrypted_attribute]] = lockbox_attribute[:attribute]
|
99
|
+
end
|
100
|
+
|
101
|
+
inspection = []
|
102
|
+
# use serializable_hash like Devise
|
103
|
+
values = serializable_hash
|
104
|
+
self.class.attribute_names.each do |k|
|
105
|
+
next if !has_attribute?(k) || lockbox_attributes[k]
|
106
|
+
|
107
|
+
# check for lockbox attribute
|
108
|
+
if lockbox_encrypted_attributes[k]
|
109
|
+
# check if ciphertext attribute nil to avoid loading attribute
|
110
|
+
v = send(k).nil? ? "nil" : "[FILTERED]"
|
111
|
+
k = lockbox_encrypted_attributes[k]
|
112
|
+
elsif values.key?(k)
|
113
|
+
v = respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : values[k].inspect
|
114
|
+
|
115
|
+
# fix for https://github.com/rails/rails/issues/40725
|
116
|
+
# TODO only apply to Active Record 6.0
|
117
|
+
if respond_to?(:inspection_filter, true) && v != "nil"
|
118
|
+
v = inspection_filter.filter_param(k, v)
|
119
|
+
end
|
120
|
+
else
|
121
|
+
next
|
79
122
|
end
|
123
|
+
|
124
|
+
inspection << "#{k}: #{v}"
|
125
|
+
end
|
126
|
+
|
80
127
|
"#<#{self.class} #{inspection.join(", ")}>"
|
81
128
|
end
|
82
129
|
|
@@ -164,6 +211,7 @@ module Lockbox
|
|
164
211
|
end
|
165
212
|
|
166
213
|
raise "Duplicate encrypted attribute: #{original_name}" if lockbox_attributes[original_name]
|
214
|
+
raise "Multiple encrypted attributes use the same column: #{encrypted_attribute}" if lockbox_attributes.any? { |_, v| v[:encrypted_attribute] == encrypted_attribute }
|
167
215
|
@lockbox_attributes[original_name] = options
|
168
216
|
|
169
217
|
if activerecord
|
@@ -199,6 +247,18 @@ module Lockbox
|
|
199
247
|
else
|
200
248
|
attribute name, :string
|
201
249
|
end
|
250
|
+
else
|
251
|
+
# hack for Active Record 6.1
|
252
|
+
# to set string type after serialize
|
253
|
+
# otherwise, type gets set to ActiveModel::Type::Value
|
254
|
+
# which always returns false for changed_in_place?
|
255
|
+
# earlier versions of Active Record take the previous code path
|
256
|
+
if ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
|
257
|
+
attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call
|
258
|
+
if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
|
259
|
+
attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
|
260
|
+
end
|
261
|
+
end
|
202
262
|
end
|
203
263
|
|
204
264
|
define_method("#{name}_was") do
|
@@ -338,7 +398,7 @@ module Lockbox
|
|
338
398
|
table = activerecord ? table_name : collection_name.to_s
|
339
399
|
|
340
400
|
unless message.nil?
|
341
|
-
# TODO use attribute type class in 0.
|
401
|
+
# TODO use attribute type class in 0.7.0
|
342
402
|
case options[:type]
|
343
403
|
when :boolean
|
344
404
|
message = ActiveRecord::Type::Boolean.new.serialize(message)
|
@@ -393,7 +453,7 @@ module Lockbox
|
|
393
453
|
end
|
394
454
|
|
395
455
|
unless message.nil?
|
396
|
-
# TODO use attribute type class in 0.
|
456
|
+
# TODO use attribute type class in 0.7.0
|
397
457
|
case options[:type]
|
398
458
|
when :boolean
|
399
459
|
message = message == "t"
|
data/lib/lockbox/railtie.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
module Lockbox
|
2
2
|
class Railtie < Rails::Railtie
|
3
3
|
initializer "lockbox" do |app|
|
4
|
+
if defined?(Rails.application.credentials)
|
5
|
+
Lockbox.master_key ||= Rails.application.credentials.dig(:lockbox, :master_key)
|
6
|
+
end
|
7
|
+
|
4
8
|
require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
|
5
9
|
|
6
10
|
if defined?(ActiveStorage)
|
data/lib/lockbox/utils.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module Lockbox
|
2
2
|
class Utils
|
3
3
|
def self.build_box(context, options, table, attribute)
|
4
|
+
# dup options (with except) since keys are sometimes changed or deleted
|
4
5
|
options = options.except(:attribute, :encrypted_attribute, :migrating, :attached, :type)
|
5
6
|
options[:encode] = false unless options.key?(:encode)
|
6
7
|
options.each do |k, v|
|
@@ -16,14 +17,32 @@ module Lockbox
|
|
16
17
|
end
|
17
18
|
|
18
19
|
unless options[:key] || options[:encryption_key] || options[:decryption_key]
|
19
|
-
options[:key] =
|
20
|
+
options[:key] =
|
21
|
+
Lockbox.attribute_key(
|
22
|
+
table: options.delete(:key_table) || table,
|
23
|
+
attribute: options.delete(:key_attribute) || attribute,
|
24
|
+
master_key: options.delete(:master_key),
|
25
|
+
encode: false
|
26
|
+
)
|
20
27
|
end
|
21
28
|
|
22
29
|
if options[:previous_versions].is_a?(Array)
|
23
|
-
|
30
|
+
# dup previous versions array (with map) since elements are updated
|
31
|
+
# dup each version (with dup) since keys are sometimes deleted
|
32
|
+
options[:previous_versions] = options[:previous_versions].map(&:dup)
|
24
33
|
options[:previous_versions].each_with_index do |version, i|
|
25
|
-
if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
|
26
|
-
|
34
|
+
if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && (version[:master_key] || version[:key_table] || version[:key_attribute])
|
35
|
+
# could also use key_table and key_attribute from options
|
36
|
+
# when specified, but keep simple for now
|
37
|
+
# also, this change isn't backward compatible
|
38
|
+
key =
|
39
|
+
Lockbox.attribute_key(
|
40
|
+
table: version.delete(:key_table) || table,
|
41
|
+
attribute: version.delete(:key_attribute) || attribute,
|
42
|
+
master_key: version.delete(:master_key),
|
43
|
+
encode: false
|
44
|
+
)
|
45
|
+
options[:previous_versions][i] = version.merge(key: key)
|
27
46
|
end
|
28
47
|
end
|
29
48
|
end
|
@@ -40,7 +59,7 @@ module Lockbox
|
|
40
59
|
key = [key].pack("H*")
|
41
60
|
end
|
42
61
|
|
43
|
-
raise Lockbox::Error, "#{name} must be
|
62
|
+
raise Lockbox::Error, "#{name} must be #{size} bytes (#{size * 2} hex digits)" if key.bytesize != size
|
44
63
|
raise Lockbox::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
|
45
64
|
|
46
65
|
key
|
@@ -70,13 +89,11 @@ module Lockbox
|
|
70
89
|
attachable = attachable.dup
|
71
90
|
attachable[:io] = box.encrypt_io(io)
|
72
91
|
else
|
73
|
-
|
74
|
-
raise NotImplementedError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
|
92
|
+
raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
|
75
93
|
end
|
76
94
|
|
77
95
|
# don't analyze encrypted data
|
78
|
-
metadata = {"analyzed" => true}
|
79
|
-
metadata["encrypted"] = true if options[:migrating]
|
96
|
+
metadata = {"analyzed" => true, "encrypted" => true}
|
80
97
|
attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
|
81
98
|
end
|
82
99
|
|
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
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -150,6 +150,34 @@ dependencies:
|
|
150
150
|
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: shrine
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: shrine-mongoid
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
153
181
|
- !ruby/object:Gem::Dependency
|
154
182
|
name: benchmark-ips
|
155
183
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,7 +192,7 @@ dependencies:
|
|
164
192
|
- - ">="
|
165
193
|
- !ruby/object:Gem::Version
|
166
194
|
version: '0'
|
167
|
-
description:
|
195
|
+
description:
|
168
196
|
email: andrew@chartkick.com
|
169
197
|
executables: []
|
170
198
|
extensions: []
|
@@ -197,7 +225,7 @@ homepage: https://github.com/ankane/lockbox
|
|
197
225
|
licenses:
|
198
226
|
- MIT
|
199
227
|
metadata: {}
|
200
|
-
post_install_message:
|
228
|
+
post_install_message:
|
201
229
|
rdoc_options: []
|
202
230
|
require_paths:
|
203
231
|
- lib
|
@@ -212,8 +240,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
240
|
- !ruby/object:Gem::Version
|
213
241
|
version: '0'
|
214
242
|
requirements: []
|
215
|
-
rubygems_version: 3.1.
|
216
|
-
signing_key:
|
243
|
+
rubygems_version: 3.1.4
|
244
|
+
signing_key:
|
217
245
|
specification_version: 4
|
218
|
-
summary: Modern encryption for Rails
|
246
|
+
summary: Modern encryption for Ruby and Rails
|
219
247
|
test_files: []
|