lockbox 0.4.7 → 0.6.1
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 +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
|
-
[](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: []
|