lockbox 0.4.6 → 0.4.7
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 +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
|