lockbox 0.4.6 → 0.4.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +70 -5
- data/lib/lockbox/active_storage_extensions.rb +29 -4
- data/lib/lockbox/carrier_wave_extensions.rb +19 -0
- data/lib/lockbox/migrator.rb +7 -0
- data/lib/lockbox/model.rb +10 -6
- data/lib/lockbox/utils.rb +2 -2
- data/lib/lockbox/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f8c447dd90537203a1a3038c347c10de0e48f5b29795b382a2a77019e6e5764
|
4
|
+
data.tar.gz: 9133e9eb0c2132b7c77c39f8c24a3c27ea9b3cbb1d3d82f7f069b2db9992198f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4396ee4ead0de0592e7a3574b563f98d58f0402198dd8662cbdc374cc5f8a39e629f6397414b498456c665e8a635518db3a532056ecd42154206fde4ab938e5c
|
7
|
+
data.tar.gz: 6057ea6f43db261580a0ee0ae13f1303251d2ee8ef2b1bcdba8399b6a858dca1fdf6c84e25f5da85a059a6d260be7094969e2118ff11a8c1d2565617c48f74cd
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -190,6 +190,8 @@ end
|
|
190
190
|
|
191
191
|
## Action Text
|
192
192
|
|
193
|
+
**Note:** Action Text uses direct uploads for files, which cannot be encrypted with application-level encryption like Lockbox. This only encrypts the database field.
|
194
|
+
|
193
195
|
Create a migration with:
|
194
196
|
|
195
197
|
```ruby
|
@@ -264,8 +266,9 @@ end
|
|
264
266
|
|
265
267
|
There are a few limitations to be aware of:
|
266
268
|
|
267
|
-
-
|
268
|
-
-
|
269
|
+
- Variants and previews aren’t supported when encrypted
|
270
|
+
- Metadata like image width and height aren’t extracted when encrypted
|
271
|
+
- Direct uploads can’t be encrypted with application-level encryption like Lockbox, but can use server-side encryption
|
269
272
|
|
270
273
|
To serve encrypted files, use a controller action.
|
271
274
|
|
@@ -508,6 +511,24 @@ Lockbox.rotate(User, attributes: [:email])
|
|
508
511
|
|
509
512
|
Once all records are rotated, you can remove `previous_versions` from the model.
|
510
513
|
|
514
|
+
### Action Text
|
515
|
+
|
516
|
+
Update your initializer:
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
Lockbox.encrypts_action_text_body(previous_versions: [{key: previous_key}])
|
520
|
+
```
|
521
|
+
|
522
|
+
Use `master_key` instead of `key` if passing the master key.
|
523
|
+
|
524
|
+
To rotate existing records, use:
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
Lockbox.rotate(ActionText::RichText, attributes: [:body])
|
528
|
+
```
|
529
|
+
|
530
|
+
Once all records are rotated, you can remove `previous_versions` from the initializer.
|
531
|
+
|
511
532
|
### Active Storage
|
512
533
|
|
513
534
|
Update your model:
|
@@ -550,6 +571,14 @@ User.find_each do |user|
|
|
550
571
|
end
|
551
572
|
```
|
552
573
|
|
574
|
+
For multiple files, use:
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
User.find_each do |user|
|
578
|
+
user.licenses.map(&:rotate_encryption!)
|
579
|
+
end
|
580
|
+
```
|
581
|
+
|
553
582
|
Once all files are rotated, you can remove `previous_versions` from the model.
|
554
583
|
|
555
584
|
### Local Files & Strings
|
@@ -734,12 +763,32 @@ end
|
|
734
763
|
|
735
764
|
You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
|
736
765
|
|
766
|
+
For Active Record and Mongoid, use:
|
767
|
+
|
737
768
|
```ruby
|
738
769
|
class User < ApplicationRecord
|
739
770
|
encrypts :email, key: :kms_key
|
740
771
|
end
|
741
772
|
```
|
742
773
|
|
774
|
+
For Action Text, use:
|
775
|
+
|
776
|
+
```ruby
|
777
|
+
ActiveSupport.on_load(:action_text_rich_text) do
|
778
|
+
ActionText::RichText.has_kms_key
|
779
|
+
end
|
780
|
+
|
781
|
+
Lockbox.encrypts_action_text_body(key: :kms_key)
|
782
|
+
```
|
783
|
+
|
784
|
+
For Active Storage, use:
|
785
|
+
|
786
|
+
```ruby
|
787
|
+
class User < ApplicationRecord
|
788
|
+
encrypts_attached :license, key: :kms_key
|
789
|
+
end
|
790
|
+
```
|
791
|
+
|
743
792
|
For CarrierWave, use:
|
744
793
|
|
745
794
|
```ruby
|
@@ -772,7 +821,7 @@ lockbox.encrypt("clear").bytesize # 44
|
|
772
821
|
lockbox.encrypt("consider").bytesize # 44
|
773
822
|
```
|
774
823
|
|
775
|
-
The block size for padding is 16 bytes by default.
|
824
|
+
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
825
|
|
777
826
|
```ruby
|
778
827
|
box.encrypt("length15status!").bytesize # 44
|
@@ -785,9 +834,25 @@ Change the block size with:
|
|
785
834
|
Lockbox.new(padding: 32) # bytes
|
786
835
|
```
|
787
836
|
|
837
|
+
## Associated Data
|
838
|
+
|
839
|
+
You can pass extra context during encryption to make sure encrypted data isn’t moved to a different context.
|
840
|
+
|
841
|
+
```ruby
|
842
|
+
lockbox = Lockbox.new(key: key)
|
843
|
+
ciphertext = lockbox.encrypt(message, associated_data: "somecontext")
|
844
|
+
```
|
845
|
+
|
846
|
+
Without the same context, decryption will fail.
|
847
|
+
|
848
|
+
```ruby
|
849
|
+
lockbox.decrypt(ciphertext, associated_data: "somecontext") # success
|
850
|
+
lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
|
851
|
+
```
|
852
|
+
|
788
853
|
## Binary Columns
|
789
854
|
|
790
|
-
You can use `binary` columns for the ciphertext instead of `text` columns
|
855
|
+
You can use `binary` columns for the ciphertext instead of `text` columns.
|
791
856
|
|
792
857
|
```ruby
|
793
858
|
class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
|
@@ -797,7 +862,7 @@ class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
|
|
797
862
|
end
|
798
863
|
```
|
799
864
|
|
800
|
-
|
865
|
+
Disable Base64 encoding to save space.
|
801
866
|
|
802
867
|
```ruby
|
803
868
|
class User < ApplicationRecord
|
@@ -1,7 +1,22 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
1
|
+
# Ideally encryption and decryption would happen at the blob/service level.
|
2
|
+
# However, Active Storage < 6.1 only supports a single service (per environment).
|
3
|
+
# This means all attachments need to be encrypted or none of them,
|
4
|
+
# which is often not practical.
|
5
|
+
#
|
6
|
+
# Active Storage 6.1 adds support for multiple services, which changes this.
|
7
|
+
# We could have a Lockbox service:
|
8
|
+
#
|
9
|
+
# lockbox:
|
10
|
+
# service: Lockbox
|
11
|
+
# backend: local # delegate to another service, like mirror service
|
12
|
+
# key: ... # Lockbox options
|
13
|
+
#
|
14
|
+
# However, the checksum is computed *and stored on the blob*
|
15
|
+
# before the file is passed to the service.
|
16
|
+
# We don't want the MD5 checksum of the plaintext stored in the database.
|
17
|
+
#
|
18
|
+
# Instead, we encrypt and decrypt at the attachment level,
|
19
|
+
# and we define encryption settings at the model level.
|
5
20
|
module Lockbox
|
6
21
|
module ActiveStorageExtensions
|
7
22
|
module Attached
|
@@ -95,6 +110,16 @@ module Lockbox
|
|
95
110
|
result
|
96
111
|
end
|
97
112
|
|
113
|
+
def variant(*args)
|
114
|
+
raise Lockbox::Error, "Variant not supported for encrypted files" if Utils.encrypted_options(record, name)
|
115
|
+
super
|
116
|
+
end
|
117
|
+
|
118
|
+
def preview(*args)
|
119
|
+
raise Lockbox::Error, "Preview not supported for encrypted files" if Utils.encrypted_options(record, name)
|
120
|
+
super
|
121
|
+
end
|
122
|
+
|
98
123
|
if ActiveStorage::VERSION::MAJOR >= 6
|
99
124
|
def open(**options)
|
100
125
|
blob.open(**options) do |file|
|
@@ -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
|
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
@@ -87,6 +87,9 @@ module Lockbox
|
|
87
87
|
# essentially a no-op if already loaded
|
88
88
|
# an exception is thrown if decryption fails
|
89
89
|
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
90
|
+
# don't try to decrypt if no decryption key given
|
91
|
+
next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
|
92
|
+
|
90
93
|
# it is possible that the encrypted attribute is not loaded, eg.
|
91
94
|
# if the record was fetched partially (`User.select(:id).first`).
|
92
95
|
# accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
|
@@ -263,12 +266,13 @@ module Lockbox
|
|
263
266
|
define_method("#{name}=") do |message|
|
264
267
|
# decrypt first for dirty tracking
|
265
268
|
# don't raise error if can't decrypt previous
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
269
|
+
# don't try to decrypt if no decryption key given
|
270
|
+
unless options[:algorithm] == "hybrid" && options[:decryption_key].nil?
|
271
|
+
begin
|
272
|
+
send(name)
|
273
|
+
rescue Lockbox::DecryptionError
|
274
|
+
warn "[lockbox] Decrypting previous value failed"
|
275
|
+
end
|
272
276
|
end
|
273
277
|
|
274
278
|
send("lockbox_direct_#{name}=", message)
|
data/lib/lockbox/utils.rb
CHANGED
@@ -16,14 +16,14 @@ module Lockbox
|
|
16
16
|
end
|
17
17
|
|
18
18
|
unless options[:key] || options[:encryption_key] || options[:decryption_key]
|
19
|
-
options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key))
|
19
|
+
options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key), encode: false)
|
20
20
|
end
|
21
21
|
|
22
22
|
if options[:previous_versions].is_a?(Array)
|
23
23
|
options[:previous_versions] = options[:previous_versions].dup
|
24
24
|
options[:previous_versions].each_with_index do |version, i|
|
25
25
|
if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
|
26
|
-
options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key)))
|
26
|
+
options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key), encode: false))
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
data/lib/lockbox/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lockbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|