lockbox 0.4.8 → 0.6.2
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/LICENSE.txt +1 -1
- data/README.md +118 -33
- data/lib/generators/lockbox/audits_generator.rb +11 -3
- data/lib/lockbox.rb +6 -0
- 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 +81 -12
- data/lib/lockbox/railtie.rb +5 -0
- data/lib/lockbox/utils.rb +19 -8
- data/lib/lockbox/version.rb +1 -1
- metadata +10 -164
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ba37bc916c02e18555640f29e47483898a96e04b75639b49ce9a0003fbaa443
|
4
|
+
data.tar.gz: 750a53ca3201e51b6dc0221305d4c021d64716a163a2e0cfc98c11ec8d1b6af8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b4e23752c311bf6b161e6817ab204ba0b5936594db5c2509c3f5ef6e354c169097a1c799d7b5147daa0d68982f072d1d22363019c6881566403517f97a5f2c45
|
7
|
+
data.tar.gz: 51ab913facdc34aea3e3ef263dab2fa5c3852aa052de80804ec29019c2517df9244eefb8c15e6f2c1ca0059bfd4f9cc020ce00fff543874f6461faeec1e78443
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,35 @@
|
|
1
|
+
## 0.6.2 (2020-02-08)
|
2
|
+
|
3
|
+
- Added `inet` type
|
4
|
+
- Fixed error when `lockbox` key in Rails credentials has a string value
|
5
|
+
- Fixed deprecation warning with Active Record 6.1
|
6
|
+
|
7
|
+
## 0.6.1 (2020-12-03)
|
8
|
+
|
9
|
+
- Added integration with Rails credentials
|
10
|
+
- Fixed in place changes for Active Record 6.1
|
11
|
+
- Fixed error with `content_type` method for CarrierWave < 2
|
12
|
+
|
13
|
+
## 0.6.0 (2020-12-03)
|
14
|
+
|
15
|
+
- Added `encrypted` flag to Active Storage metadata
|
16
|
+
- Added encrypted columns to `filter_attributes`
|
17
|
+
- Improved `inspect` method
|
18
|
+
|
19
|
+
## 0.5.0 (2020-11-22)
|
20
|
+
|
21
|
+
- Improved error messages for hybrid cryptography
|
22
|
+
- Changed warning to error when no attributes specified
|
23
|
+
- Fixed issue with `pluck` when migrating
|
24
|
+
- Fixed error with `key_table` and `key_attribute` options with `previous_versions`
|
25
|
+
|
26
|
+
## 0.4.9 (2020-10-01)
|
27
|
+
|
28
|
+
- Added `key_table` and `key_attribute` options to `previous_versions`
|
29
|
+
- Added `encrypted_attribute` option
|
30
|
+
- Added support for encrypting empty string
|
31
|
+
- Improved `inspect` for models with encrypted attributes
|
32
|
+
|
1
33
|
## 0.4.8 (2020-08-30)
|
2
34
|
|
3
35
|
- Added `key_table` and `key_attribute` options
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# Lockbox
|
2
2
|
|
3
|
-
:package: Modern encryption for Rails
|
3
|
+
:package: Modern encryption for Ruby and Rails
|
4
4
|
|
5
5
|
- Works with database fields, files, and strings
|
6
6
|
- Maximizes compatibility with existing code and libraries
|
7
7
|
- Makes migrating existing data and key rotation easy
|
8
|
+
- Has zero dependencies and many integrations
|
8
9
|
|
9
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).
|
10
11
|
|
11
|
-
[![Build Status](https://
|
12
|
+
[![Build Status](https://github.com/ankane/lockbox/workflows/build/badge.svg?branch=master)](https://github.com/ankane/lockbox/actions)
|
12
13
|
|
13
14
|
## Installation
|
14
15
|
|
@@ -26,7 +27,7 @@ Generate a key
|
|
26
27
|
Lockbox.generate_key
|
27
28
|
```
|
28
29
|
|
29
|
-
Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production.
|
30
|
+
Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production.
|
30
31
|
|
31
32
|
Set the following environment variable with your key (you can use this one in development)
|
32
33
|
|
@@ -34,10 +35,17 @@ Set the following environment variable with your key (you can use this one in de
|
|
34
35
|
LOCKBOX_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
35
36
|
```
|
36
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
|
+
|
37
45
|
or create `config/initializers/lockbox.rb` with something like
|
38
46
|
|
39
47
|
```ruby
|
40
|
-
Lockbox.master_key = Rails.application.credentials.
|
48
|
+
Lockbox.master_key = Rails.application.credentials.lockbox[:master_key]
|
41
49
|
```
|
42
50
|
|
43
51
|
Then follow the instructions below for the data you want to encrypt.
|
@@ -113,6 +121,7 @@ class User < ApplicationRecord
|
|
113
121
|
encrypts :properties, type: :json
|
114
122
|
encrypts :settings, type: :hash
|
115
123
|
encrypts :messages, type: :array
|
124
|
+
encrypts :ip, type: :inet
|
116
125
|
end
|
117
126
|
```
|
118
127
|
|
@@ -196,6 +205,34 @@ class User < ApplicationRecord
|
|
196
205
|
end
|
197
206
|
```
|
198
207
|
|
208
|
+
#### Model Changes
|
209
|
+
|
210
|
+
If tracking changes to model attributes, be sure to remove or redact encrypted attributes.
|
211
|
+
|
212
|
+
PaperTrail
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class User < ApplicationRecord
|
216
|
+
# for an encrypted history (still tracks ciphertext changes)
|
217
|
+
has_paper_trail skip: [:email]
|
218
|
+
|
219
|
+
# for no history (add blind indexes as well)
|
220
|
+
has_paper_trail skip: [:email, :email_ciphertext]
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
Audited
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
class User < ApplicationRecord
|
228
|
+
# for an encrypted history (still tracks ciphertext changes)
|
229
|
+
audited except: [:email]
|
230
|
+
|
231
|
+
# for no history (add blind indexes as well)
|
232
|
+
audited except: [:email, :email_ciphertext]
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
199
236
|
#### Decryption
|
200
237
|
|
201
238
|
To decrypt data outside the model, use:
|
@@ -413,44 +450,58 @@ Finally, delete the unencrypted files and drop the column for the original uploa
|
|
413
450
|
|
414
451
|
## Shrine
|
415
452
|
|
416
|
-
|
453
|
+
#### Models
|
454
|
+
|
455
|
+
Include the attachment as normal:
|
417
456
|
|
418
457
|
```ruby
|
419
|
-
|
458
|
+
class User < ApplicationRecord
|
459
|
+
include LicenseUploader::Attachment(:license)
|
460
|
+
end
|
420
461
|
```
|
421
462
|
|
422
|
-
|
463
|
+
And encrypt in a controller (or background job, etc) with:
|
423
464
|
|
424
465
|
```ruby
|
425
|
-
|
466
|
+
license = params.require(:user).fetch(:license)
|
467
|
+
lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
|
468
|
+
user.license = lockbox.encrypt_io(license)
|
426
469
|
```
|
427
470
|
|
428
|
-
|
471
|
+
To serve encrypted files, use a controller action.
|
429
472
|
|
430
473
|
```ruby
|
431
|
-
|
474
|
+
def license
|
475
|
+
user = User.find(params[:id])
|
476
|
+
lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
|
477
|
+
send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
|
478
|
+
end
|
432
479
|
```
|
433
480
|
|
434
|
-
|
481
|
+
#### Non-Models
|
482
|
+
|
483
|
+
Generate a key
|
435
484
|
|
436
485
|
```ruby
|
437
|
-
|
486
|
+
key = Lockbox.generate_key
|
438
487
|
```
|
439
488
|
|
440
|
-
|
489
|
+
Create a lockbox
|
441
490
|
|
442
491
|
```ruby
|
443
|
-
|
444
|
-
user.license = lockbox.encrypt_io(license)
|
492
|
+
lockbox = Lockbox.new(key: key)
|
445
493
|
```
|
446
494
|
|
447
|
-
|
495
|
+
Encrypt files before passing them to Shrine
|
448
496
|
|
449
497
|
```ruby
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
498
|
+
LicenseUploader.upload(lockbox.encrypt_io(file), :store)
|
499
|
+
```
|
500
|
+
|
501
|
+
And decrypt them after reading
|
502
|
+
|
503
|
+
```ruby
|
504
|
+
lockbox.decrypt(uploaded_file.read)
|
454
505
|
```
|
455
506
|
|
456
507
|
## Local Files
|
@@ -655,6 +706,8 @@ This is the default algorithm. It’s:
|
|
655
706
|
- an IETF standard
|
656
707
|
- fast thanks to a [dedicated instruction set](https://en.wikipedia.org/wiki/AES_instruction_set)
|
657
708
|
|
709
|
+
Lockbox uses 256-bit keys.
|
710
|
+
|
658
711
|
**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.
|
659
712
|
|
660
713
|
### XSalsa20
|
@@ -708,6 +761,22 @@ For Ubuntu 16.04, use:
|
|
708
761
|
sudo apt-get install libsodium18
|
709
762
|
```
|
710
763
|
|
764
|
+
##### GitHub Actions
|
765
|
+
|
766
|
+
For Ubuntu 20.04 and 18.04, use:
|
767
|
+
|
768
|
+
```yml
|
769
|
+
- name: Install Libsodium
|
770
|
+
run: sudo apt-get update && sudo apt-get install libsodium23
|
771
|
+
```
|
772
|
+
|
773
|
+
For Ubuntu 16.04, use:
|
774
|
+
|
775
|
+
```yml
|
776
|
+
- name: Install Libsodium
|
777
|
+
run: sudo apt-get update && sudo apt-get install libsodium18
|
778
|
+
```
|
779
|
+
|
711
780
|
##### Travis CI
|
712
781
|
|
713
782
|
On Bionic, add to `.travis.yml`:
|
@@ -735,8 +804,7 @@ Add a step to `.circleci/config.yml`:
|
|
735
804
|
```yml
|
736
805
|
- run:
|
737
806
|
name: install Libsodium
|
738
|
-
command:
|
739
|
-
sudo apt-get install -y libsodium18
|
807
|
+
command: sudo apt-get install -y libsodium18
|
740
808
|
```
|
741
809
|
|
742
810
|
## Hybrid Cryptography
|
@@ -773,7 +841,9 @@ Lockbox supports a few different ways to set keys for database fields and files.
|
|
773
841
|
|
774
842
|
### Master Key
|
775
843
|
|
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.
|
844
|
+
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.
|
845
|
+
|
846
|
+
You can get an individual key with:
|
777
847
|
|
778
848
|
```ruby
|
779
849
|
Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
|
@@ -820,10 +890,14 @@ To use a different key for each record, use a symbol:
|
|
820
890
|
```ruby
|
821
891
|
class User < ApplicationRecord
|
822
892
|
encrypts :email, key: :some_method
|
893
|
+
end
|
894
|
+
```
|
823
895
|
|
824
|
-
|
825
|
-
|
826
|
-
|
896
|
+
Or a proc:
|
897
|
+
|
898
|
+
```ruby
|
899
|
+
class User < ApplicationRecord
|
900
|
+
encrypts :email, key: -> { some_method }
|
827
901
|
end
|
828
902
|
```
|
829
903
|
|
@@ -938,12 +1012,6 @@ class User < ApplicationRecord
|
|
938
1012
|
end
|
939
1013
|
```
|
940
1014
|
|
941
|
-
or set it globally:
|
942
|
-
|
943
|
-
```ruby
|
944
|
-
Lockbox.default_options = {encode: false}
|
945
|
-
```
|
946
|
-
|
947
1015
|
## Compatibility
|
948
1016
|
|
949
1017
|
It’s easy to read encrypted data in another language if needed.
|
@@ -1019,12 +1087,29 @@ end
|
|
1019
1087
|
|
1020
1088
|
## Upgrading
|
1021
1089
|
|
1090
|
+
### 0.6.0
|
1091
|
+
|
1092
|
+
0.6.0 adds `encrypted: true` to Active Storage metadata for new files. This field is informational, but if you prefer to add it to existing files, use:
|
1093
|
+
|
1094
|
+
```ruby
|
1095
|
+
User.with_attached_license.find_each do |user|
|
1096
|
+
next unless user.license.attached?
|
1097
|
+
|
1098
|
+
metadata = user.license.metadata
|
1099
|
+
unless metadata["encrypted"]
|
1100
|
+
user.license.blob.update!(metadata: metadata.merge("encrypted" => true))
|
1101
|
+
end
|
1102
|
+
end
|
1103
|
+
```
|
1104
|
+
|
1022
1105
|
### 0.3.6
|
1023
1106
|
|
1024
1107
|
0.3.6 makes content type detection more reliable for Active Storage. You can check and update the content type of existing files with:
|
1025
1108
|
|
1026
1109
|
```ruby
|
1027
|
-
User.find_each do |user|
|
1110
|
+
User.with_attached_license.find_each do |user|
|
1111
|
+
next unless user.license.attached?
|
1112
|
+
|
1028
1113
|
license = user.license
|
1029
1114
|
content_type = Marcel::MimeType.for(license.download, name: license.filename.to_s)
|
1030
1115
|
if content_type != license.content_type
|
@@ -16,9 +16,7 @@ module Lockbox
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def data_type
|
19
|
-
|
20
|
-
# so database connection isn't needed
|
21
|
-
case ActiveRecord::Base.connection_config[:adapter].to_s
|
19
|
+
case adapter
|
22
20
|
when /postg/i # postgres, postgis
|
23
21
|
"jsonb"
|
24
22
|
when /mysql/i
|
@@ -27,6 +25,16 @@ module Lockbox
|
|
27
25
|
"text"
|
28
26
|
end
|
29
27
|
end
|
28
|
+
|
29
|
+
# use connection_config instead of connection.adapter
|
30
|
+
# so database connection isn't needed
|
31
|
+
def adapter
|
32
|
+
if ActiveRecord::VERSION::STRING.to_f >= 6.1
|
33
|
+
ActiveRecord::Base.connection_db_config.adapter.to_s
|
34
|
+
else
|
35
|
+
ActiveRecord::Base.connection_config[:adapter].to_s
|
36
|
+
end
|
37
|
+
end
|
30
38
|
end
|
31
39
|
end
|
32
40
|
end
|
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"
|
@@ -26,6 +27,11 @@ end
|
|
26
27
|
|
27
28
|
if defined?(ActiveSupport.on_load)
|
28
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
|
+
|
29
35
|
extend Lockbox::Model
|
30
36
|
extend Lockbox::Model::Attached
|
31
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
@@ -22,21 +22,25 @@ module Lockbox
|
|
22
22
|
# end
|
23
23
|
|
24
24
|
custom_type = options[:type].respond_to?(:serialize) && options[:type].respond_to?(:deserialize)
|
25
|
-
|
25
|
+
valid_types = [nil, :string, :boolean, :date, :datetime, :time, :integer, :float, :binary, :json, :hash, :array, :inet]
|
26
|
+
raise ArgumentError, "Unknown type: #{options[:type]}" unless custom_type || valid_types.include?(options[:type])
|
26
27
|
|
27
28
|
activerecord = defined?(ActiveRecord::Base) && self < ActiveRecord::Base
|
28
29
|
raise ArgumentError, "Type not supported yet with Mongoid" if options[:type] && !activerecord
|
29
30
|
|
30
|
-
|
31
|
-
warn "[lockbox] WARNING: No attributes specified" if attributes.empty?
|
31
|
+
raise ArgumentError, "No attributes specified" if attributes.empty?
|
32
32
|
|
33
33
|
raise ArgumentError, "Cannot use key_attribute with multiple attributes" if options[:key_attribute] && attributes.size > 1
|
34
34
|
|
35
|
+
original_options = options.dup
|
36
|
+
|
35
37
|
attributes.each do |name|
|
36
|
-
#
|
37
|
-
|
38
|
+
# per attribute options
|
39
|
+
# TODO use a different name
|
40
|
+
options = original_options.dup
|
38
41
|
|
39
|
-
|
42
|
+
# add default options
|
43
|
+
encrypted_attribute = options.delete(:encrypted_attribute) || "#{name}_ciphertext"
|
40
44
|
|
41
45
|
# migrating
|
42
46
|
original_name = name.to_sym
|
@@ -52,6 +56,15 @@ module Lockbox
|
|
52
56
|
decrypt_method_name = "decrypt_#{encrypted_attribute}"
|
53
57
|
|
54
58
|
class_eval do
|
59
|
+
# Lockbox uses custom inspect
|
60
|
+
# but this could be useful for other gems
|
61
|
+
if activerecord && ActiveRecord::VERSION::MAJOR >= 6
|
62
|
+
# only add virtual attribute
|
63
|
+
# need to use regexp since strings do partial matching
|
64
|
+
# also, need to use += instead of <<
|
65
|
+
self.filter_attributes += [/\A#{Regexp.escape(options[:attribute])}\z/]
|
66
|
+
end
|
67
|
+
|
55
68
|
@lockbox_attributes ||= {}
|
56
69
|
|
57
70
|
if @lockbox_attributes.empty?
|
@@ -76,12 +89,42 @@ module Lockbox
|
|
76
89
|
super(options)
|
77
90
|
end
|
78
91
|
|
79
|
-
#
|
92
|
+
# maintain order
|
93
|
+
# replace ciphertext attributes w/ virtual attributes (filtered)
|
80
94
|
def inspect
|
81
|
-
|
82
|
-
|
83
|
-
|
95
|
+
lockbox_attributes = {}
|
96
|
+
lockbox_encrypted_attributes = {}
|
97
|
+
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
98
|
+
lockbox_attributes[lockbox_attribute[:attribute]] = true
|
99
|
+
lockbox_encrypted_attributes[lockbox_attribute[:encrypted_attribute]] = lockbox_attribute[:attribute]
|
100
|
+
end
|
101
|
+
|
102
|
+
inspection = []
|
103
|
+
# use serializable_hash like Devise
|
104
|
+
values = serializable_hash
|
105
|
+
self.class.attribute_names.each do |k|
|
106
|
+
next if !has_attribute?(k) || lockbox_attributes[k]
|
107
|
+
|
108
|
+
# check for lockbox attribute
|
109
|
+
if lockbox_encrypted_attributes[k]
|
110
|
+
# check if ciphertext attribute nil to avoid loading attribute
|
111
|
+
v = send(k).nil? ? "nil" : "[FILTERED]"
|
112
|
+
k = lockbox_encrypted_attributes[k]
|
113
|
+
elsif values.key?(k)
|
114
|
+
v = respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : values[k].inspect
|
115
|
+
|
116
|
+
# fix for https://github.com/rails/rails/issues/40725
|
117
|
+
# TODO only apply to Active Record 6.0
|
118
|
+
if respond_to?(:inspection_filter, true) && v != "nil"
|
119
|
+
v = inspection_filter.filter_param(k, v)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
next
|
84
123
|
end
|
124
|
+
|
125
|
+
inspection << "#{k}: #{v}"
|
126
|
+
end
|
127
|
+
|
85
128
|
"#<#{self.class} #{inspection.join(", ")}>"
|
86
129
|
end
|
87
130
|
|
@@ -169,6 +212,7 @@ module Lockbox
|
|
169
212
|
end
|
170
213
|
|
171
214
|
raise "Duplicate encrypted attribute: #{original_name}" if lockbox_attributes[original_name]
|
215
|
+
raise "Multiple encrypted attributes use the same column: #{encrypted_attribute}" if lockbox_attributes.any? { |_, v| v[:encrypted_attribute] == encrypted_attribute }
|
172
216
|
@lockbox_attributes[original_name] = options
|
173
217
|
|
174
218
|
if activerecord
|
@@ -204,6 +248,18 @@ module Lockbox
|
|
204
248
|
else
|
205
249
|
attribute name, :string
|
206
250
|
end
|
251
|
+
else
|
252
|
+
# hack for Active Record 6.1
|
253
|
+
# to set string type after serialize
|
254
|
+
# otherwise, type gets set to ActiveModel::Type::Value
|
255
|
+
# which always returns false for changed_in_place?
|
256
|
+
# earlier versions of Active Record take the previous code path
|
257
|
+
if ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
|
258
|
+
attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call
|
259
|
+
if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
|
260
|
+
attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
|
261
|
+
end
|
262
|
+
end
|
207
263
|
end
|
208
264
|
|
209
265
|
define_method("#{name}_was") do
|
@@ -343,7 +399,7 @@ module Lockbox
|
|
343
399
|
table = activerecord ? table_name : collection_name.to_s
|
344
400
|
|
345
401
|
unless message.nil?
|
346
|
-
# TODO use attribute type class in 0.
|
402
|
+
# TODO use attribute type class in 0.7.0
|
347
403
|
case options[:type]
|
348
404
|
when :boolean
|
349
405
|
message = ActiveRecord::Type::Boolean.new.serialize(message)
|
@@ -371,6 +427,14 @@ module Lockbox
|
|
371
427
|
message = ActiveRecord::Type::Float.new.serialize(message)
|
372
428
|
# double precision, big endian
|
373
429
|
message = [message].pack("G") unless message.nil?
|
430
|
+
when :inet
|
431
|
+
unless message.nil?
|
432
|
+
ip = message.is_a?(IPAddr) ? message : (IPAddr.new(message) rescue nil)
|
433
|
+
# same format as Postgres, with ipv4 padded to 16 bytes
|
434
|
+
# family, netmask, ip
|
435
|
+
# return nil for invalid IP like Active Record
|
436
|
+
message = ip ? [ip.ipv4? ? 0 : 1, ip.prefix, ip.hton].pack("CCa16") : nil
|
437
|
+
end
|
374
438
|
when :string, :binary
|
375
439
|
# do nothing
|
376
440
|
# encrypt will convert to binary
|
@@ -398,7 +462,7 @@ module Lockbox
|
|
398
462
|
end
|
399
463
|
|
400
464
|
unless message.nil?
|
401
|
-
# TODO use attribute type class in 0.
|
465
|
+
# TODO use attribute type class in 0.7.0
|
402
466
|
case options[:type]
|
403
467
|
when :boolean
|
404
468
|
message = message == "t"
|
@@ -417,6 +481,11 @@ module Lockbox
|
|
417
481
|
when :binary
|
418
482
|
# do nothing
|
419
483
|
# decrypt returns binary string
|
484
|
+
when :inet
|
485
|
+
family, prefix, addr = message.unpack("CCa16")
|
486
|
+
len = family == 0 ? 4 : 16
|
487
|
+
message = IPAddr.new_ntoh(addr.first(len))
|
488
|
+
message.prefix = prefix
|
420
489
|
else
|
421
490
|
# use original name for serialized attributes
|
422
491
|
type = (try(:attribute_types) || {})[original_name.to_s]
|
data/lib/lockbox/railtie.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
module Lockbox
|
2
2
|
class Railtie < Rails::Railtie
|
3
3
|
initializer "lockbox" do |app|
|
4
|
+
if defined?(Rails.application.credentials)
|
5
|
+
# needs to work when lockbox key has a string value
|
6
|
+
Lockbox.master_key ||= Rails.application.credentials.try(:lockbox).try(:fetch, :master_key, nil)
|
7
|
+
end
|
8
|
+
|
4
9
|
require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
|
5
10
|
|
6
11
|
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|
|
@@ -26,10 +27,22 @@ module Lockbox
|
|
26
27
|
end
|
27
28
|
|
28
29
|
if options[:previous_versions].is_a?(Array)
|
29
|
-
|
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)
|
30
33
|
options[:previous_versions].each_with_index do |version, i|
|
31
|
-
if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
|
32
|
-
|
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)
|
33
46
|
end
|
34
47
|
end
|
35
48
|
end
|
@@ -46,7 +59,7 @@ module Lockbox
|
|
46
59
|
key = [key].pack("H*")
|
47
60
|
end
|
48
61
|
|
49
|
-
raise Lockbox::Error, "#{name} must be
|
62
|
+
raise Lockbox::Error, "#{name} must be #{size} bytes (#{size * 2} hex digits)" if key.bytesize != size
|
50
63
|
raise Lockbox::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
|
51
64
|
|
52
65
|
key
|
@@ -76,13 +89,11 @@ module Lockbox
|
|
76
89
|
attachable = attachable.dup
|
77
90
|
attachable[:io] = box.encrypt_io(io)
|
78
91
|
else
|
79
|
-
|
80
|
-
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}"
|
81
93
|
end
|
82
94
|
|
83
95
|
# don't analyze encrypted data
|
84
|
-
metadata = {"analyzed" => true}
|
85
|
-
metadata["encrypted"] = true if options[:migrating]
|
96
|
+
metadata = {"analyzed" => true, "encrypted" => true}
|
86
97
|
attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
|
87
98
|
end
|
88
99
|
|
data/lib/lockbox/version.rb
CHANGED
metadata
CHANGED
@@ -1,171 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lockbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.2
|
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:
|
12
|
-
dependencies:
|
13
|
-
|
14
|
-
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: carrierwave
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: combustion
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.3'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '1.3'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rails
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: minitest
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '5'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '5'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rake
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: rbnacl
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '6'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '6'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: sqlite3
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: pg
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - ">="
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '0'
|
132
|
-
type: :development
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - ">="
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '0'
|
139
|
-
- !ruby/object:Gem::Dependency
|
140
|
-
name: mysql2
|
141
|
-
requirement: !ruby/object:Gem::Requirement
|
142
|
-
requirements:
|
143
|
-
- - ">="
|
144
|
-
- !ruby/object:Gem::Version
|
145
|
-
version: '0'
|
146
|
-
type: :development
|
147
|
-
prerelease: false
|
148
|
-
version_requirements: !ruby/object:Gem::Requirement
|
149
|
-
requirements:
|
150
|
-
- - ">="
|
151
|
-
- !ruby/object:Gem::Version
|
152
|
-
version: '0'
|
153
|
-
- !ruby/object:Gem::Dependency
|
154
|
-
name: benchmark-ips
|
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
|
-
description:
|
168
|
-
email: andrew@chartkick.com
|
11
|
+
date: 2021-02-08 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: andrew@ankane.org
|
169
15
|
executables: []
|
170
16
|
extensions: []
|
171
17
|
extra_rdoc_files: []
|
@@ -197,7 +43,7 @@ homepage: https://github.com/ankane/lockbox
|
|
197
43
|
licenses:
|
198
44
|
- MIT
|
199
45
|
metadata: {}
|
200
|
-
post_install_message:
|
46
|
+
post_install_message:
|
201
47
|
rdoc_options: []
|
202
48
|
require_paths:
|
203
49
|
- lib
|
@@ -212,8 +58,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
212
58
|
- !ruby/object:Gem::Version
|
213
59
|
version: '0'
|
214
60
|
requirements: []
|
215
|
-
rubygems_version: 3.
|
216
|
-
signing_key:
|
61
|
+
rubygems_version: 3.2.3
|
62
|
+
signing_key:
|
217
63
|
specification_version: 4
|
218
|
-
summary: Modern encryption for Rails
|
64
|
+
summary: Modern encryption for Ruby and Rails
|
219
65
|
test_files: []
|