lockbox 0.4.6 → 0.6.0
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 +31 -0
- data/README.md +230 -37
- data/lib/lockbox.rb +4 -1
- data/lib/lockbox/active_storage_extensions.rb +33 -4
- 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 +23 -0
- data/lib/lockbox/migrator.rb +7 -0
- data/lib/lockbox/model.rb +67 -15
- 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: 77945cdf065bda9282a4a9cbffd77ebe518bcbe11c3e3ccaa91768bd1579f94c
|
|
4
|
+
data.tar.gz: ab052f812a91e1620dcc52ac578006b55e84d5aae1770ecb5c6c89c5119f9073
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8223d89af7efa1e4192c48512a45177d877df2c1878da425b05bf4e2b066c1f3a413b57f8f0fbdc73a04f000db191bfde2ef8bb0319c4bce7eee6a1985a3771f
|
|
7
|
+
data.tar.gz: 7bcb4b647f4abdc8141c571441ce1c8e47b5864aee3e043cd7f620ca7f9abc208ddb1134ac6f35700c73a30f934c56169ff5bca235ba32faba4a0d2325bc926a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
## 0.6.0 (2020-12-03)
|
|
2
|
+
|
|
3
|
+
- Added `encrypted` flag to Active Storage metadata
|
|
4
|
+
- Added encrypted columns to `filter_attributes`
|
|
5
|
+
- Improved `inspect` method
|
|
6
|
+
|
|
7
|
+
## 0.5.0 (2020-11-22)
|
|
8
|
+
|
|
9
|
+
- Improved error messages for hybrid cryptography
|
|
10
|
+
- Changed warning to error when no attributes specified
|
|
11
|
+
- Fixed issue with `pluck` when migrating
|
|
12
|
+
- Fixed error with `key_table` and `key_attribute` options with `previous_versions`
|
|
13
|
+
|
|
14
|
+
## 0.4.9 (2020-10-01)
|
|
15
|
+
|
|
16
|
+
- Added `key_table` and `key_attribute` options to `previous_versions`
|
|
17
|
+
- Added `encrypted_attribute` option
|
|
18
|
+
- Added support for encrypting empty string
|
|
19
|
+
- Improved `inspect` for models with encrypted attributes
|
|
20
|
+
|
|
21
|
+
## 0.4.8 (2020-08-30)
|
|
22
|
+
|
|
23
|
+
- Added `key_table` and `key_attribute` options
|
|
24
|
+
- Added warning when no attributes specified
|
|
25
|
+
- Fixed error when Active Support partially loaded
|
|
26
|
+
|
|
27
|
+
## 0.4.7 (2020-08-18)
|
|
28
|
+
|
|
29
|
+
- Added `lockbox_options` method to encrypted CarrierWave uploaders
|
|
30
|
+
- Improved attribute loading when no decryption key specified
|
|
31
|
+
|
|
1
32
|
## 0.4.6 (2020-07-02)
|
|
2
33
|
|
|
3
34
|
- Added support for `update_column` and `update_columns`
|
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
|
|
|
@@ -89,6 +88,16 @@ User.create!(email: "hi@example.org")
|
|
|
89
88
|
|
|
90
89
|
If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
|
|
91
90
|
|
|
91
|
+
#### Multiple Fields
|
|
92
|
+
|
|
93
|
+
You can specify multiple fields in single line.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
class User < ApplicationRecord
|
|
97
|
+
encrypts :email, :phone, :city
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
92
101
|
#### Types
|
|
93
102
|
|
|
94
103
|
Fields are strings by default. Specify the type of a field with:
|
|
@@ -188,8 +197,46 @@ class User < ApplicationRecord
|
|
|
188
197
|
end
|
|
189
198
|
```
|
|
190
199
|
|
|
200
|
+
#### Model Changes
|
|
201
|
+
|
|
202
|
+
If tracking changes to model attributes, be sure to remove or redact encrypted attributes.
|
|
203
|
+
|
|
204
|
+
PaperTrail
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
class User < ApplicationRecord
|
|
208
|
+
# for an encrypted history (still tracks ciphertext changes)
|
|
209
|
+
has_paper_trail skip: [:email]
|
|
210
|
+
|
|
211
|
+
# for no history (add blind indexes as well)
|
|
212
|
+
has_paper_trail skip: [:email, :email_ciphertext]
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Audited
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
class User < ApplicationRecord
|
|
220
|
+
# for an encrypted history (still tracks ciphertext changes)
|
|
221
|
+
audited except: [:email]
|
|
222
|
+
|
|
223
|
+
# for no history (add blind indexes as well)
|
|
224
|
+
audited except: [:email, :email_ciphertext]
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### Decryption
|
|
229
|
+
|
|
230
|
+
To decrypt data outside the model, use:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
User.decrypt_email_ciphertext(user.email_ciphertext)
|
|
234
|
+
```
|
|
235
|
+
|
|
191
236
|
## Action Text
|
|
192
237
|
|
|
238
|
+
**Note:** Action Text uses direct uploads for files, which cannot be encrypted with application-level encryption like Lockbox. This only encrypts the database field.
|
|
239
|
+
|
|
193
240
|
Create a migration with:
|
|
194
241
|
|
|
195
242
|
```ruby
|
|
@@ -220,6 +267,10 @@ Lockbox.encrypts_action_text_body
|
|
|
220
267
|
|
|
221
268
|
And drop the unencrypted column.
|
|
222
269
|
|
|
270
|
+
#### Options
|
|
271
|
+
|
|
272
|
+
You can pass any Lockbox options to the `encrypts_action_text_body` method.
|
|
273
|
+
|
|
223
274
|
## Mongoid
|
|
224
275
|
|
|
225
276
|
Add to your model:
|
|
@@ -264,8 +315,9 @@ end
|
|
|
264
315
|
|
|
265
316
|
There are a few limitations to be aware of:
|
|
266
317
|
|
|
267
|
-
-
|
|
268
|
-
-
|
|
318
|
+
- Variants and previews aren’t supported when encrypted
|
|
319
|
+
- Metadata like image width and height aren’t extracted when encrypted
|
|
320
|
+
- Direct uploads can’t be encrypted with application-level encryption like Lockbox, but can use server-side encryption
|
|
269
321
|
|
|
270
322
|
To serve encrypted files, use a controller action.
|
|
271
323
|
|
|
@@ -390,44 +442,58 @@ Finally, delete the unencrypted files and drop the column for the original uploa
|
|
|
390
442
|
|
|
391
443
|
## Shrine
|
|
392
444
|
|
|
393
|
-
|
|
445
|
+
#### Models
|
|
446
|
+
|
|
447
|
+
Include the attachment as normal:
|
|
394
448
|
|
|
395
449
|
```ruby
|
|
396
|
-
|
|
450
|
+
class User < ApplicationRecord
|
|
451
|
+
include LicenseUploader::Attachment(:license)
|
|
452
|
+
end
|
|
397
453
|
```
|
|
398
454
|
|
|
399
|
-
|
|
455
|
+
And encrypt in a controller (or background job, etc) with:
|
|
400
456
|
|
|
401
457
|
```ruby
|
|
402
|
-
|
|
458
|
+
license = params.require(:user).fetch(:license)
|
|
459
|
+
lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
|
|
460
|
+
user.license = lockbox.encrypt_io(license)
|
|
403
461
|
```
|
|
404
462
|
|
|
405
|
-
|
|
463
|
+
To serve encrypted files, use a controller action.
|
|
406
464
|
|
|
407
465
|
```ruby
|
|
408
|
-
|
|
466
|
+
def license
|
|
467
|
+
user = User.find(params[:id])
|
|
468
|
+
lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
|
|
469
|
+
send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
|
|
470
|
+
end
|
|
409
471
|
```
|
|
410
472
|
|
|
411
|
-
|
|
473
|
+
#### Non-Models
|
|
474
|
+
|
|
475
|
+
Generate a key
|
|
412
476
|
|
|
413
477
|
```ruby
|
|
414
|
-
|
|
478
|
+
key = Lockbox.generate_key
|
|
415
479
|
```
|
|
416
480
|
|
|
417
|
-
|
|
481
|
+
Create a lockbox
|
|
418
482
|
|
|
419
483
|
```ruby
|
|
420
|
-
|
|
421
|
-
user.license = lockbox.encrypt_io(license)
|
|
484
|
+
lockbox = Lockbox.new(key: key)
|
|
422
485
|
```
|
|
423
486
|
|
|
424
|
-
|
|
487
|
+
Encrypt files before passing them to Shrine
|
|
425
488
|
|
|
426
489
|
```ruby
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
490
|
+
LicenseUploader.upload(lockbox.encrypt_io(file), :store)
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
And decrypt them after reading
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
lockbox.decrypt(uploaded_file.read)
|
|
431
497
|
```
|
|
432
498
|
|
|
433
499
|
## Local Files
|
|
@@ -508,6 +574,24 @@ Lockbox.rotate(User, attributes: [:email])
|
|
|
508
574
|
|
|
509
575
|
Once all records are rotated, you can remove `previous_versions` from the model.
|
|
510
576
|
|
|
577
|
+
### Action Text
|
|
578
|
+
|
|
579
|
+
Update your initializer:
|
|
580
|
+
|
|
581
|
+
```ruby
|
|
582
|
+
Lockbox.encrypts_action_text_body(previous_versions: [{key: previous_key}])
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Use `master_key` instead of `key` if passing the master key.
|
|
586
|
+
|
|
587
|
+
To rotate existing records, use:
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
Lockbox.rotate(ActionText::RichText, attributes: [:body])
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Once all records are rotated, you can remove `previous_versions` from the initializer.
|
|
594
|
+
|
|
511
595
|
### Active Storage
|
|
512
596
|
|
|
513
597
|
Update your model:
|
|
@@ -550,6 +634,14 @@ User.find_each do |user|
|
|
|
550
634
|
end
|
|
551
635
|
```
|
|
552
636
|
|
|
637
|
+
For multiple files, use:
|
|
638
|
+
|
|
639
|
+
```ruby
|
|
640
|
+
User.find_each do |user|
|
|
641
|
+
user.licenses.map(&:rotate_encryption!)
|
|
642
|
+
end
|
|
643
|
+
```
|
|
644
|
+
|
|
553
645
|
Once all files are rotated, you can remove `previous_versions` from the model.
|
|
554
646
|
|
|
555
647
|
### Local Files & Strings
|
|
@@ -606,6 +698,8 @@ This is the default algorithm. It’s:
|
|
|
606
698
|
- an IETF standard
|
|
607
699
|
- fast thanks to a [dedicated instruction set](https://en.wikipedia.org/wiki/AES_instruction_set)
|
|
608
700
|
|
|
701
|
+
Lockbox uses 256-bit keys.
|
|
702
|
+
|
|
609
703
|
**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.
|
|
610
704
|
|
|
611
705
|
### XSalsa20
|
|
@@ -659,6 +753,22 @@ For Ubuntu 16.04, use:
|
|
|
659
753
|
sudo apt-get install libsodium18
|
|
660
754
|
```
|
|
661
755
|
|
|
756
|
+
##### GitHub Actions
|
|
757
|
+
|
|
758
|
+
For Ubuntu 20.04 and 18.04, use:
|
|
759
|
+
|
|
760
|
+
```yml
|
|
761
|
+
- name: Install Libsodium
|
|
762
|
+
run: sudo apt-get update && sudo apt-get install libsodium23
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
For Ubuntu 16.04, use:
|
|
766
|
+
|
|
767
|
+
```yml
|
|
768
|
+
- name: Install Libsodium
|
|
769
|
+
run: sudo apt-get update && sudo apt-get install libsodium18
|
|
770
|
+
```
|
|
771
|
+
|
|
662
772
|
##### Travis CI
|
|
663
773
|
|
|
664
774
|
On Bionic, add to `.travis.yml`:
|
|
@@ -686,8 +796,7 @@ Add a step to `.circleci/config.yml`:
|
|
|
686
796
|
```yml
|
|
687
797
|
- run:
|
|
688
798
|
name: install Libsodium
|
|
689
|
-
command:
|
|
690
|
-
sudo apt-get install -y libsodium18
|
|
799
|
+
command: sudo apt-get install -y libsodium18
|
|
691
800
|
```
|
|
692
801
|
|
|
693
802
|
## Hybrid Cryptography
|
|
@@ -714,15 +823,43 @@ Make sure `decryption_key` is `nil` on servers that shouldn’t decrypt.
|
|
|
714
823
|
|
|
715
824
|
This uses X25519 for key exchange and XSalsa20 for encryption.
|
|
716
825
|
|
|
717
|
-
## Key
|
|
826
|
+
## Key Configuration
|
|
827
|
+
|
|
828
|
+
Lockbox supports a few different ways to set keys for database fields and files.
|
|
829
|
+
|
|
830
|
+
1. Master key
|
|
831
|
+
2. Per field/uploader
|
|
832
|
+
3. Per record
|
|
718
833
|
|
|
719
|
-
|
|
834
|
+
### Master Key
|
|
835
|
+
|
|
836
|
+
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.
|
|
837
|
+
|
|
838
|
+
You can get an individual key with:
|
|
720
839
|
|
|
721
840
|
```ruby
|
|
722
841
|
Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
|
|
723
842
|
```
|
|
724
843
|
|
|
725
|
-
|
|
844
|
+
To rename a table with encrypted columns/uploaders, use:
|
|
845
|
+
|
|
846
|
+
```ruby
|
|
847
|
+
class User < ApplicationRecord
|
|
848
|
+
encrypts :email, key_table: "original_table"
|
|
849
|
+
end
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
To rename an encrypted column itself, use:
|
|
853
|
+
|
|
854
|
+
```ruby
|
|
855
|
+
class User < ApplicationRecord
|
|
856
|
+
encrypts :email, key_attribute: "original_column"
|
|
857
|
+
end
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### Per Field/Uploader
|
|
861
|
+
|
|
862
|
+
To set a key for an individual field/uploader, use a string:
|
|
726
863
|
|
|
727
864
|
```ruby
|
|
728
865
|
class User < ApplicationRecord
|
|
@@ -730,16 +867,62 @@ class User < ApplicationRecord
|
|
|
730
867
|
end
|
|
731
868
|
```
|
|
732
869
|
|
|
870
|
+
Or a proc:
|
|
871
|
+
|
|
872
|
+
```ruby
|
|
873
|
+
class User < ApplicationRecord
|
|
874
|
+
encrypts :email, key: -> { code }
|
|
875
|
+
end
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
### Per Record
|
|
879
|
+
|
|
880
|
+
To use a different key for each record, use a symbol:
|
|
881
|
+
|
|
882
|
+
```ruby
|
|
883
|
+
class User < ApplicationRecord
|
|
884
|
+
encrypts :email, key: :some_method
|
|
885
|
+
end
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
Or a proc:
|
|
889
|
+
|
|
890
|
+
```ruby
|
|
891
|
+
class User < ApplicationRecord
|
|
892
|
+
encrypts :email, key: -> { some_method }
|
|
893
|
+
end
|
|
894
|
+
```
|
|
895
|
+
|
|
733
896
|
## Key Management
|
|
734
897
|
|
|
735
898
|
You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
|
|
736
899
|
|
|
900
|
+
For Active Record and Mongoid, use:
|
|
901
|
+
|
|
737
902
|
```ruby
|
|
738
903
|
class User < ApplicationRecord
|
|
739
904
|
encrypts :email, key: :kms_key
|
|
740
905
|
end
|
|
741
906
|
```
|
|
742
907
|
|
|
908
|
+
For Action Text, use:
|
|
909
|
+
|
|
910
|
+
```ruby
|
|
911
|
+
ActiveSupport.on_load(:action_text_rich_text) do
|
|
912
|
+
ActionText::RichText.has_kms_key
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
Lockbox.encrypts_action_text_body(key: :kms_key)
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
For Active Storage, use:
|
|
919
|
+
|
|
920
|
+
```ruby
|
|
921
|
+
class User < ApplicationRecord
|
|
922
|
+
encrypts_attached :license, key: :kms_key
|
|
923
|
+
end
|
|
924
|
+
```
|
|
925
|
+
|
|
743
926
|
For CarrierWave, use:
|
|
744
927
|
|
|
745
928
|
```ruby
|
|
@@ -772,7 +955,7 @@ lockbox.encrypt("clear").bytesize # 44
|
|
|
772
955
|
lockbox.encrypt("consider").bytesize # 44
|
|
773
956
|
```
|
|
774
957
|
|
|
775
|
-
The block size for padding is 16 bytes by default.
|
|
958
|
+
The block size for padding is 16 bytes by default. Lockbox uses [ISO/IEC 7816-4](https://en.wikipedia.org/wiki/Padding_(cryptography)#ISO/IEC_7816-4) padding, which uses at least one byte, so if we have a status larger than 15 bytes, it will have a different length than the others.
|
|
776
959
|
|
|
777
960
|
```ruby
|
|
778
961
|
box.encrypt("length15status!").bytesize # 44
|
|
@@ -785,9 +968,25 @@ Change the block size with:
|
|
|
785
968
|
Lockbox.new(padding: 32) # bytes
|
|
786
969
|
```
|
|
787
970
|
|
|
971
|
+
## Associated Data
|
|
972
|
+
|
|
973
|
+
You can pass extra context during encryption to make sure encrypted data isn’t moved to a different context.
|
|
974
|
+
|
|
975
|
+
```ruby
|
|
976
|
+
lockbox = Lockbox.new(key: key)
|
|
977
|
+
ciphertext = lockbox.encrypt(message, associated_data: "somecontext")
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
Without the same context, decryption will fail.
|
|
981
|
+
|
|
982
|
+
```ruby
|
|
983
|
+
lockbox.decrypt(ciphertext, associated_data: "somecontext") # success
|
|
984
|
+
lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
|
|
985
|
+
```
|
|
986
|
+
|
|
788
987
|
## Binary Columns
|
|
789
988
|
|
|
790
|
-
You can use `binary` columns for the ciphertext instead of `text` columns
|
|
989
|
+
You can use `binary` columns for the ciphertext instead of `text` columns.
|
|
791
990
|
|
|
792
991
|
```ruby
|
|
793
992
|
class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
|
|
@@ -797,7 +996,7 @@ class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
|
|
|
797
996
|
end
|
|
798
997
|
```
|
|
799
998
|
|
|
800
|
-
|
|
999
|
+
Disable Base64 encoding to save space.
|
|
801
1000
|
|
|
802
1001
|
```ruby
|
|
803
1002
|
class User < ApplicationRecord
|
|
@@ -805,12 +1004,6 @@ class User < ApplicationRecord
|
|
|
805
1004
|
end
|
|
806
1005
|
```
|
|
807
1006
|
|
|
808
|
-
or set it globally:
|
|
809
|
-
|
|
810
|
-
```ruby
|
|
811
|
-
Lockbox.default_options = {encode: false}
|
|
812
|
-
```
|
|
813
|
-
|
|
814
1007
|
## Compatibility
|
|
815
1008
|
|
|
816
1009
|
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,10 +20,12 @@ 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
|
|
27
30
|
extend Lockbox::Model
|
|
28
31
|
extend Lockbox::Model::Attached
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
1
|
+
# Ideally encryption and decryption would happen at the blob/service level.
|
|
2
|
+
# However, Active Storage < 6.1 only supports a single service (per environment).
|
|
3
|
+
# This means all attachments need to be encrypted or none of them,
|
|
4
|
+
# which is often not practical.
|
|
5
|
+
#
|
|
6
|
+
# Active Storage 6.1 adds support for multiple services, which changes this.
|
|
7
|
+
# We could have a Lockbox service:
|
|
8
|
+
#
|
|
9
|
+
# lockbox:
|
|
10
|
+
# service: Lockbox
|
|
11
|
+
# backend: local # delegate to another service, like mirror service
|
|
12
|
+
# key: ... # Lockbox options
|
|
13
|
+
#
|
|
14
|
+
# However, the checksum is computed *and stored on the blob*
|
|
15
|
+
# before the file is passed to the service.
|
|
16
|
+
# We don't want the MD5 checksum of the plaintext stored in the database.
|
|
17
|
+
#
|
|
18
|
+
# Instead, we encrypt and decrypt at the attachment level,
|
|
19
|
+
# and we define encryption settings at the model level.
|
|
5
20
|
module Lockbox
|
|
6
21
|
module ActiveStorageExtensions
|
|
7
22
|
module Attached
|
|
@@ -74,6 +89,10 @@ module Lockbox
|
|
|
74
89
|
module CreateOne
|
|
75
90
|
def initialize(name, record, attachable)
|
|
76
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
|
|
77
96
|
attachable = Lockbox::Utils.encrypt_attachable(record, name, attachable) if Lockbox::Utils.encrypted?(record, name) && !attachable.is_a?(ActiveStorage::Blob)
|
|
78
97
|
super(name, record, attachable)
|
|
79
98
|
end
|
|
@@ -95,6 +114,16 @@ module Lockbox
|
|
|
95
114
|
result
|
|
96
115
|
end
|
|
97
116
|
|
|
117
|
+
def variant(*args)
|
|
118
|
+
raise Lockbox::Error, "Variant not supported for encrypted files" if Utils.encrypted_options(record, name)
|
|
119
|
+
super
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def preview(*args)
|
|
123
|
+
raise Lockbox::Error, "Preview not supported for encrypted files" if Utils.encrypted_options(record, name)
|
|
124
|
+
super
|
|
125
|
+
end
|
|
126
|
+
|
|
98
127
|
if ActiveStorage::VERSION::MAJOR >= 6
|
|
99
128
|
def open(**options)
|
|
100
129
|
blob.open(**options) do |file|
|
data/lib/lockbox/aes_gcm.rb
CHANGED
|
@@ -18,9 +18,10 @@ module Lockbox
|
|
|
18
18
|
# In encryption mode, it must be set after calling #encrypt and setting #key= and #iv=
|
|
19
19
|
cipher.auth_data = associated_data || ""
|
|
20
20
|
|
|
21
|
-
ciphertext =
|
|
21
|
+
ciphertext = String.new
|
|
22
|
+
ciphertext << cipher.update(message) unless message.empty?
|
|
23
|
+
ciphertext << cipher.final
|
|
22
24
|
ciphertext << cipher.auth_tag
|
|
23
|
-
|
|
24
25
|
ciphertext
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -29,7 +30,6 @@ module Lockbox
|
|
|
29
30
|
|
|
30
31
|
fail_decryption if nonce.to_s.bytesize != nonce_bytes
|
|
31
32
|
fail_decryption if auth_tag.to_s.bytesize != auth_tag_bytes
|
|
32
|
-
fail_decryption if ciphertext.to_s.bytesize == 0
|
|
33
33
|
|
|
34
34
|
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
35
35
|
# do not change order of operations
|
|
@@ -43,7 +43,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
|
|
@@ -2,9 +2,22 @@ module Lockbox
|
|
|
2
2
|
module CarrierWaveExtensions
|
|
3
3
|
def encrypt(**options)
|
|
4
4
|
class_eval do
|
|
5
|
+
# uses same hook as process (before cache)
|
|
6
|
+
# processing can be disabled, so better to keep separate
|
|
5
7
|
before :cache, :encrypt
|
|
6
8
|
|
|
9
|
+
define_singleton_method :lockbox_options do
|
|
10
|
+
options
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
def encrypt(file)
|
|
14
|
+
# safety check
|
|
15
|
+
# see CarrierWave::Uploader::Cache#cache!
|
|
16
|
+
raise Lockbox::Error, "Expected files to be equal. Please report an issue." unless file && @file && file == @file
|
|
17
|
+
|
|
18
|
+
# processors in CarrierWave move updated file to current_path
|
|
19
|
+
# however, this causes versions to use the processed file
|
|
20
|
+
# we only want to change the file for the current version
|
|
8
21
|
@file = CarrierWave::SanitizedFile.new(lockbox_notify("encrypt_file") { lockbox.encrypt_io(file) })
|
|
9
22
|
end
|
|
10
23
|
|
|
@@ -14,6 +27,7 @@ module Lockbox
|
|
|
14
27
|
lockbox_notify("decrypt_file") { lockbox.decrypt(r) } if r
|
|
15
28
|
end
|
|
16
29
|
|
|
30
|
+
# use size of plaintext since read and content type use plaintext
|
|
17
31
|
def size
|
|
18
32
|
read.bytesize
|
|
19
33
|
end
|
|
@@ -23,6 +37,7 @@ module Lockbox
|
|
|
23
37
|
MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
|
|
24
38
|
end
|
|
25
39
|
|
|
40
|
+
# disable processing since already processed
|
|
26
41
|
def rotate_encryption!
|
|
27
42
|
io = Lockbox::IO.new(read)
|
|
28
43
|
io.original_filename = file.filename
|
|
@@ -46,6 +61,8 @@ module Lockbox
|
|
|
46
61
|
end
|
|
47
62
|
end
|
|
48
63
|
|
|
64
|
+
# for mounted uploaders, use mounted name
|
|
65
|
+
# for others, use uploader name
|
|
49
66
|
def lockbox_name
|
|
50
67
|
if mounted_as
|
|
51
68
|
mounted_as.to_s
|
|
@@ -58,6 +75,8 @@ module Lockbox
|
|
|
58
75
|
end
|
|
59
76
|
end
|
|
60
77
|
|
|
78
|
+
# Active Support notifications so it's easier
|
|
79
|
+
# to see when files are encrypted and decrypted
|
|
61
80
|
def lockbox_notify(type)
|
|
62
81
|
if defined?(ActiveSupport::Notifications)
|
|
63
82
|
name = lockbox_name
|
|
@@ -78,4 +97,8 @@ module Lockbox
|
|
|
78
97
|
end
|
|
79
98
|
end
|
|
80
99
|
|
|
100
|
+
if CarrierWave::VERSION.to_i > 2
|
|
101
|
+
raise "CarrierWave version not supported in this version of Lockbox: #{CarrierWave::VERSION}"
|
|
102
|
+
end
|
|
103
|
+
|
|
81
104
|
CarrierWave::Uploader::Base.extend(Lockbox::CarrierWaveExtensions)
|
data/lib/lockbox/migrator.rb
CHANGED
|
@@ -116,6 +116,13 @@ module Lockbox
|
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# there's a small chance for this process to read data,
|
|
120
|
+
# another process to update the data, and
|
|
121
|
+
# this process to write the now stale data
|
|
122
|
+
# this time window can be reduced with smaller batch sizes
|
|
123
|
+
# locking individual records could eliminate this
|
|
124
|
+
# one option is: relation.in_batches { |batch| batch.lock }
|
|
125
|
+
# which runs SELECT ... FOR UPDATE in Postgres
|
|
119
126
|
def migrate_records(records, fields:, blind_indexes:, restart:, rotate:)
|
|
120
127
|
# do computation outside of transaction
|
|
121
128
|
# especially expensive blind index computation
|
data/lib/lockbox/model.rb
CHANGED
|
@@ -27,11 +27,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
|
|
|
@@ -87,6 +134,9 @@ module Lockbox
|
|
|
87
134
|
# essentially a no-op if already loaded
|
|
88
135
|
# an exception is thrown if decryption fails
|
|
89
136
|
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
|
137
|
+
# don't try to decrypt if no decryption key given
|
|
138
|
+
next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
|
|
139
|
+
|
|
90
140
|
# it is possible that the encrypted attribute is not loaded, eg.
|
|
91
141
|
# if the record was fetched partially (`User.select(:id).first`).
|
|
92
142
|
# accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
|
|
@@ -161,6 +211,7 @@ module Lockbox
|
|
|
161
211
|
end
|
|
162
212
|
|
|
163
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 }
|
|
164
215
|
@lockbox_attributes[original_name] = options
|
|
165
216
|
|
|
166
217
|
if activerecord
|
|
@@ -263,12 +314,13 @@ module Lockbox
|
|
|
263
314
|
define_method("#{name}=") do |message|
|
|
264
315
|
# decrypt first for dirty tracking
|
|
265
316
|
# don't raise error if can't decrypt previous
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
317
|
+
# don't try to decrypt if no decryption key given
|
|
318
|
+
unless options[:algorithm] == "hybrid" && options[:decryption_key].nil?
|
|
319
|
+
begin
|
|
320
|
+
send(name)
|
|
321
|
+
rescue Lockbox::DecryptionError
|
|
322
|
+
warn "[lockbox] Decrypting previous value failed"
|
|
323
|
+
end
|
|
272
324
|
end
|
|
273
325
|
|
|
274
326
|
send("lockbox_direct_#{name}=", message)
|
|
@@ -334,7 +386,7 @@ module Lockbox
|
|
|
334
386
|
table = activerecord ? table_name : collection_name.to_s
|
|
335
387
|
|
|
336
388
|
unless message.nil?
|
|
337
|
-
# TODO use attribute type class in 0.
|
|
389
|
+
# TODO use attribute type class in 0.7.0
|
|
338
390
|
case options[:type]
|
|
339
391
|
when :boolean
|
|
340
392
|
message = ActiveRecord::Type::Boolean.new.serialize(message)
|
|
@@ -389,7 +441,7 @@ module Lockbox
|
|
|
389
441
|
end
|
|
390
442
|
|
|
391
443
|
unless message.nil?
|
|
392
|
-
# TODO use attribute type class in 0.
|
|
444
|
+
# TODO use attribute type class in 0.7.0
|
|
393
445
|
case options[:type]
|
|
394
446
|
when :boolean
|
|
395
447
|
message = message == "t"
|
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.0
|
|
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-03 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: []
|