active_storage_encryption 0.1.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 +7 -0
- data/Appraisals +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +236 -0
- data/Rakefile +17 -0
- data/bin/rails +26 -0
- data/bin/rubocop +8 -0
- data/config/initializers/active_storage_encryption.rb +9 -0
- data/config/routes.rb +7 -0
- data/gemfiles/rails_7.gemfile +7 -0
- data/gemfiles/rails_7.gemfile.lock +276 -0
- data/gemfiles/rails_8.gemfile +7 -0
- data/gemfiles/rails_8.gemfile.lock +276 -0
- data/lib/active_storage/service/encrypted_disk_service.rb +10 -0
- data/lib/active_storage/service/encrypted_mirror_service.rb +10 -0
- data/lib/active_storage/service/encrypted_s3_service.rb +10 -0
- data/lib/active_storage_encryption/encrypted_blobs_controller.rb +163 -0
- data/lib/active_storage_encryption/encrypted_disk_service/v1_scheme.rb +28 -0
- data/lib/active_storage_encryption/encrypted_disk_service/v2_scheme.rb +51 -0
- data/lib/active_storage_encryption/encrypted_disk_service.rb +186 -0
- data/lib/active_storage_encryption/encrypted_mirror_service.rb +76 -0
- data/lib/active_storage_encryption/encrypted_s3_service.rb +236 -0
- data/lib/active_storage_encryption/engine.rb +7 -0
- data/lib/active_storage_encryption/overrides.rb +201 -0
- data/lib/active_storage_encryption/private_url_policy.rb +53 -0
- data/lib/active_storage_encryption/resumable_gcs_upload.rb +194 -0
- data/lib/active_storage_encryption/version.rb +5 -0
- data/lib/active_storage_encryption.rb +79 -0
- data/lib/tasks/active_storage_encryption_tasks.rake +6 -0
- data/test/active_storage_encryption_test.rb +9 -0
- data/test/dummy/Rakefile +8 -0
- data/test/dummy/app/assets/stylesheets/application.css +1 -0
- data/test/dummy/app/controllers/application_controller.rb +6 -0
- data/test/dummy/app/helpers/application_helper.rb +4 -0
- data/test/dummy/app/models/application_record.rb +5 -0
- data/test/dummy/app/views/layouts/application.html.erb +22 -0
- data/test/dummy/app/views/pwa/manifest.json.erb +22 -0
- data/test/dummy/app/views/pwa/service-worker.js +26 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +37 -0
- data/test/dummy/config/application.rb +43 -0
- data/test/dummy/config/boot.rb +7 -0
- data/test/dummy/config/credentials.yml.enc +1 -0
- data/test/dummy/config/database.yml +32 -0
- data/test/dummy/config/environment.rb +7 -0
- data/test/dummy/config/environments/development.rb +59 -0
- data/test/dummy/config/environments/production.rb +81 -0
- data/test/dummy/config/environments/test.rb +53 -0
- data/test/dummy/config/initializers/content_security_policy.rb +27 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +10 -0
- data/test/dummy/config/initializers/inflections.rb +18 -0
- data/test/dummy/config/initializers/permissions_policy.rb +15 -0
- data/test/dummy/config/locales/en.yml +31 -0
- data/test/dummy/config/master.key +1 -0
- data/test/dummy/config/puma.rb +36 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/dummy/config/storage.yml +21 -0
- data/test/dummy/config.ru +8 -0
- data/test/dummy/db/migrate/20250304023851_create_active_storage_tables.active_storage.rb +60 -0
- data/test/dummy/db/migrate/20250304023853_add_blob_encryption_key_column.rb +7 -0
- data/test/dummy/db/schema.rb +47 -0
- data/test/dummy/log/test.log +1022 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/406-unsupported-browser.html +66 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +3 -0
- data/test/dummy/storage/test.sqlite3 +0 -0
- data/test/dummy/storage/x6/pl/x6plznfuhrsyjn9pox2a6xgmcs3x +0 -0
- data/test/dummy/storage/yq/sv/yqsvw5a72b3fv719zq8a6yb7lv0j +0 -0
- data/test/integration/encrypted_blobs_controller_test.rb +400 -0
- data/test/lib/encrypted_disk_service_test.rb +387 -0
- data/test/lib/encrypted_mirror_service_test.rb +159 -0
- data/test/lib/encrypted_s3_service_test.rb +293 -0
- data/test/test_helper.rb +19 -0
- metadata +264 -0
@@ -0,0 +1,276 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
active_storage_encryption (0.1.0)
|
5
|
+
activestorage
|
6
|
+
block_cipher_kit (>= 0.0.4)
|
7
|
+
rails (>= 7.2.2.1)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
actioncable (8.0.1)
|
13
|
+
actionpack (= 8.0.1)
|
14
|
+
activesupport (= 8.0.1)
|
15
|
+
nio4r (~> 2.0)
|
16
|
+
websocket-driver (>= 0.6.1)
|
17
|
+
zeitwerk (~> 2.6)
|
18
|
+
actionmailbox (8.0.1)
|
19
|
+
actionpack (= 8.0.1)
|
20
|
+
activejob (= 8.0.1)
|
21
|
+
activerecord (= 8.0.1)
|
22
|
+
activestorage (= 8.0.1)
|
23
|
+
activesupport (= 8.0.1)
|
24
|
+
mail (>= 2.8.0)
|
25
|
+
actionmailer (8.0.1)
|
26
|
+
actionpack (= 8.0.1)
|
27
|
+
actionview (= 8.0.1)
|
28
|
+
activejob (= 8.0.1)
|
29
|
+
activesupport (= 8.0.1)
|
30
|
+
mail (>= 2.8.0)
|
31
|
+
rails-dom-testing (~> 2.2)
|
32
|
+
actionpack (8.0.1)
|
33
|
+
actionview (= 8.0.1)
|
34
|
+
activesupport (= 8.0.1)
|
35
|
+
nokogiri (>= 1.8.5)
|
36
|
+
rack (>= 2.2.4)
|
37
|
+
rack-session (>= 1.0.1)
|
38
|
+
rack-test (>= 0.6.3)
|
39
|
+
rails-dom-testing (~> 2.2)
|
40
|
+
rails-html-sanitizer (~> 1.6)
|
41
|
+
useragent (~> 0.16)
|
42
|
+
actiontext (8.0.1)
|
43
|
+
actionpack (= 8.0.1)
|
44
|
+
activerecord (= 8.0.1)
|
45
|
+
activestorage (= 8.0.1)
|
46
|
+
activesupport (= 8.0.1)
|
47
|
+
globalid (>= 0.6.0)
|
48
|
+
nokogiri (>= 1.8.5)
|
49
|
+
actionview (8.0.1)
|
50
|
+
activesupport (= 8.0.1)
|
51
|
+
builder (~> 3.1)
|
52
|
+
erubi (~> 1.11)
|
53
|
+
rails-dom-testing (~> 2.2)
|
54
|
+
rails-html-sanitizer (~> 1.6)
|
55
|
+
activejob (8.0.1)
|
56
|
+
activesupport (= 8.0.1)
|
57
|
+
globalid (>= 0.3.6)
|
58
|
+
activemodel (8.0.1)
|
59
|
+
activesupport (= 8.0.1)
|
60
|
+
activerecord (8.0.1)
|
61
|
+
activemodel (= 8.0.1)
|
62
|
+
activesupport (= 8.0.1)
|
63
|
+
timeout (>= 0.4.0)
|
64
|
+
activestorage (8.0.1)
|
65
|
+
actionpack (= 8.0.1)
|
66
|
+
activejob (= 8.0.1)
|
67
|
+
activerecord (= 8.0.1)
|
68
|
+
activesupport (= 8.0.1)
|
69
|
+
marcel (~> 1.0)
|
70
|
+
activesupport (8.0.1)
|
71
|
+
base64
|
72
|
+
benchmark (>= 0.3)
|
73
|
+
bigdecimal
|
74
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
75
|
+
connection_pool (>= 2.2.5)
|
76
|
+
drb
|
77
|
+
i18n (>= 1.6, < 2)
|
78
|
+
logger (>= 1.4.2)
|
79
|
+
minitest (>= 5.1)
|
80
|
+
securerandom (>= 0.3)
|
81
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
82
|
+
uri (>= 0.13.1)
|
83
|
+
appraisal (2.5.0)
|
84
|
+
bundler
|
85
|
+
rake
|
86
|
+
thor (>= 0.14.0)
|
87
|
+
ast (2.4.2)
|
88
|
+
aws-eventstream (1.3.1)
|
89
|
+
aws-partitions (1.1060.0)
|
90
|
+
aws-sdk-core (3.220.0)
|
91
|
+
aws-eventstream (~> 1, >= 1.3.0)
|
92
|
+
aws-partitions (~> 1, >= 1.992.0)
|
93
|
+
aws-sigv4 (~> 1.9)
|
94
|
+
base64
|
95
|
+
jmespath (~> 1, >= 1.6.1)
|
96
|
+
aws-sdk-kms (1.99.0)
|
97
|
+
aws-sdk-core (~> 3, >= 3.216.0)
|
98
|
+
aws-sigv4 (~> 1.5)
|
99
|
+
aws-sdk-s3 (1.182.0)
|
100
|
+
aws-sdk-core (~> 3, >= 3.216.0)
|
101
|
+
aws-sdk-kms (~> 1)
|
102
|
+
aws-sigv4 (~> 1.5)
|
103
|
+
aws-sigv4 (1.11.0)
|
104
|
+
aws-eventstream (~> 1, >= 1.0.2)
|
105
|
+
base64 (0.2.0)
|
106
|
+
benchmark (0.4.0)
|
107
|
+
bigdecimal (3.1.9)
|
108
|
+
block_cipher_kit (0.0.4)
|
109
|
+
builder (3.3.0)
|
110
|
+
concurrent-ruby (1.3.5)
|
111
|
+
connection_pool (2.5.0)
|
112
|
+
crass (1.0.6)
|
113
|
+
date (3.4.1)
|
114
|
+
drb (2.2.1)
|
115
|
+
erubi (1.13.1)
|
116
|
+
globalid (1.2.1)
|
117
|
+
activesupport (>= 6.1)
|
118
|
+
i18n (1.14.7)
|
119
|
+
concurrent-ruby (~> 1.0)
|
120
|
+
io-console (0.8.0)
|
121
|
+
irb (1.15.1)
|
122
|
+
pp (>= 0.6.0)
|
123
|
+
rdoc (>= 4.0.0)
|
124
|
+
reline (>= 0.4.2)
|
125
|
+
jmespath (1.6.2)
|
126
|
+
json (2.10.1)
|
127
|
+
language_server-protocol (3.17.0.4)
|
128
|
+
lint_roller (1.1.0)
|
129
|
+
logger (1.6.6)
|
130
|
+
loofah (2.24.0)
|
131
|
+
crass (~> 1.0.2)
|
132
|
+
nokogiri (>= 1.12.0)
|
133
|
+
magic_frozen_string_literal (1.2.0)
|
134
|
+
mail (2.8.1)
|
135
|
+
mini_mime (>= 0.1.1)
|
136
|
+
net-imap
|
137
|
+
net-pop
|
138
|
+
net-smtp
|
139
|
+
marcel (1.0.4)
|
140
|
+
mini_mime (1.1.5)
|
141
|
+
minitest (5.25.4)
|
142
|
+
net-http (0.6.0)
|
143
|
+
uri
|
144
|
+
net-imap (0.5.6)
|
145
|
+
date
|
146
|
+
net-protocol
|
147
|
+
net-pop (0.1.2)
|
148
|
+
net-protocol
|
149
|
+
net-protocol (0.2.2)
|
150
|
+
timeout
|
151
|
+
net-smtp (0.5.1)
|
152
|
+
net-protocol
|
153
|
+
nio4r (2.7.4)
|
154
|
+
nokogiri (1.18.3-x86_64-darwin)
|
155
|
+
racc (~> 1.4)
|
156
|
+
nokogiri (1.18.3-x86_64-linux-gnu)
|
157
|
+
racc (~> 1.4)
|
158
|
+
parallel (1.26.3)
|
159
|
+
parser (3.3.7.1)
|
160
|
+
ast (~> 2.4.1)
|
161
|
+
racc
|
162
|
+
pp (0.6.2)
|
163
|
+
prettyprint
|
164
|
+
prettyprint (0.2.0)
|
165
|
+
psych (5.2.3)
|
166
|
+
date
|
167
|
+
stringio
|
168
|
+
racc (1.8.1)
|
169
|
+
rack (3.1.11)
|
170
|
+
rack-session (2.1.0)
|
171
|
+
base64 (>= 0.1.0)
|
172
|
+
rack (>= 3.0.0)
|
173
|
+
rack-test (2.2.0)
|
174
|
+
rack (>= 1.3)
|
175
|
+
rackup (2.2.1)
|
176
|
+
rack (>= 3)
|
177
|
+
rails (8.0.1)
|
178
|
+
actioncable (= 8.0.1)
|
179
|
+
actionmailbox (= 8.0.1)
|
180
|
+
actionmailer (= 8.0.1)
|
181
|
+
actionpack (= 8.0.1)
|
182
|
+
actiontext (= 8.0.1)
|
183
|
+
actionview (= 8.0.1)
|
184
|
+
activejob (= 8.0.1)
|
185
|
+
activemodel (= 8.0.1)
|
186
|
+
activerecord (= 8.0.1)
|
187
|
+
activestorage (= 8.0.1)
|
188
|
+
activesupport (= 8.0.1)
|
189
|
+
bundler (>= 1.15.0)
|
190
|
+
railties (= 8.0.1)
|
191
|
+
rails-dom-testing (2.2.0)
|
192
|
+
activesupport (>= 5.0.0)
|
193
|
+
minitest
|
194
|
+
nokogiri (>= 1.6)
|
195
|
+
rails-html-sanitizer (1.6.2)
|
196
|
+
loofah (~> 2.21)
|
197
|
+
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
198
|
+
railties (8.0.1)
|
199
|
+
actionpack (= 8.0.1)
|
200
|
+
activesupport (= 8.0.1)
|
201
|
+
irb (~> 1.13)
|
202
|
+
rackup (>= 1.0.0)
|
203
|
+
rake (>= 12.2)
|
204
|
+
thor (~> 1.0, >= 1.2.2)
|
205
|
+
zeitwerk (~> 2.6)
|
206
|
+
rainbow (3.1.1)
|
207
|
+
rake (13.2.1)
|
208
|
+
rdoc (6.12.0)
|
209
|
+
psych (>= 4.0.0)
|
210
|
+
regexp_parser (2.10.0)
|
211
|
+
reline (0.6.0)
|
212
|
+
io-console (~> 0.5)
|
213
|
+
rubocop (1.71.2)
|
214
|
+
json (~> 2.3)
|
215
|
+
language_server-protocol (>= 3.17.0)
|
216
|
+
parallel (~> 1.10)
|
217
|
+
parser (>= 3.3.0.2)
|
218
|
+
rainbow (>= 2.2.2, < 4.0)
|
219
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
220
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
221
|
+
ruby-progressbar (~> 1.7)
|
222
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
223
|
+
rubocop-ast (1.38.1)
|
224
|
+
parser (>= 3.3.1.0)
|
225
|
+
rubocop-performance (1.23.1)
|
226
|
+
rubocop (>= 1.48.1, < 2.0)
|
227
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
228
|
+
ruby-progressbar (1.13.0)
|
229
|
+
securerandom (0.4.1)
|
230
|
+
sqlite3 (2.6.0-x86_64-darwin)
|
231
|
+
sqlite3 (2.6.0-x86_64-linux-gnu)
|
232
|
+
standard (1.45.0)
|
233
|
+
language_server-protocol (~> 3.17.0.2)
|
234
|
+
lint_roller (~> 1.0)
|
235
|
+
rubocop (~> 1.71.0)
|
236
|
+
standard-custom (~> 1.0.0)
|
237
|
+
standard-performance (~> 1.6)
|
238
|
+
standard-custom (1.0.2)
|
239
|
+
lint_roller (~> 1.0)
|
240
|
+
rubocop (~> 1.50)
|
241
|
+
standard-performance (1.6.0)
|
242
|
+
lint_roller (~> 1.1)
|
243
|
+
rubocop-performance (~> 1.23.0)
|
244
|
+
stringio (3.1.5)
|
245
|
+
thor (1.3.2)
|
246
|
+
timeout (0.4.3)
|
247
|
+
tzinfo (2.0.6)
|
248
|
+
concurrent-ruby (~> 1.0)
|
249
|
+
unicode-display_width (3.1.4)
|
250
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
251
|
+
unicode-emoji (4.0.4)
|
252
|
+
uri (1.0.3)
|
253
|
+
useragent (0.16.11)
|
254
|
+
websocket-driver (0.7.7)
|
255
|
+
base64
|
256
|
+
websocket-extensions (>= 0.1.0)
|
257
|
+
websocket-extensions (0.1.5)
|
258
|
+
zeitwerk (2.7.2)
|
259
|
+
|
260
|
+
PLATFORMS
|
261
|
+
x86_64-darwin
|
262
|
+
x86_64-linux
|
263
|
+
|
264
|
+
DEPENDENCIES
|
265
|
+
active_storage_encryption!
|
266
|
+
appraisal
|
267
|
+
aws-sdk-s3
|
268
|
+
magic_frozen_string_literal
|
269
|
+
net-http
|
270
|
+
rails (>= 8.0)
|
271
|
+
rake
|
272
|
+
sqlite3
|
273
|
+
standard (>= 1.35.1)
|
274
|
+
|
275
|
+
BUNDLED WITH
|
276
|
+
2.5.11
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Needed so that Rails can find our service definition. It will perform the following
|
4
|
+
# steps. Given an "EncryptedDisk" value of the `service:` key in the YAML, it will:
|
5
|
+
#
|
6
|
+
# * Force-require a file at "active_storage/service/encrypted_disk", from any path on the $LOAD_PATH
|
7
|
+
# * Instantiate a class called "ActiveStorage::Service::EncryptedDiskService"
|
8
|
+
require_relative "../../active_storage_encryption"
|
9
|
+
class ActiveStorage::Service::EncryptedDiskService < ActiveStorageEncryption::EncryptedDiskService
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Needed so that Rails can find our service definition. It will perform the following
|
4
|
+
# steps. Given an "EncryptedDisk" value of the `service:` key in the YAML, it will:
|
5
|
+
#
|
6
|
+
# * Force-require a file at "active_storage/service/encrypted_disk", from any path on the $LOAD_PATH
|
7
|
+
# * Instantiate a class called "ActiveStorage::Service::EncryptedDiskService"
|
8
|
+
require_relative "../../active_storage_encryption/active_storage_encryption"
|
9
|
+
class ActiveStorage::Service::EncryptedMirrorService < ActiveStorageEncryption::EncryptedMirrorService
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Needed so that Rails can find our service definition. It will perform the following
|
4
|
+
# steps. Given an "EncryptedDisk" value of the `service:` key in the YAML, it will:
|
5
|
+
#
|
6
|
+
# * Force-require a file at "active_storage/service/encrypted_disk", from any path on the $LOAD_PATH
|
7
|
+
# * Instantiate a class called "ActiveStorage::Service::EncryptedDiskService"
|
8
|
+
require_relative "../../active_storage_encryption"
|
9
|
+
class ActiveStorage::Service::EncryptedS3Service < ActiveStorageEncryption::EncryptedS3Service
|
10
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorageEncryption::EncryptedBlobsController < ActionController::Base
|
4
|
+
include ActiveStorage::SetCurrent
|
5
|
+
|
6
|
+
# Below similar to ActiveStorage::Streaming but ActionController::Live is meh.
|
7
|
+
include ActionController::DataStreaming
|
8
|
+
include ActionController::Live
|
9
|
+
|
10
|
+
class InvalidParams < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
|
14
|
+
|
15
|
+
self.etag_with_template_digest = false
|
16
|
+
skip_forgery_protection
|
17
|
+
|
18
|
+
# Accepts PUT requests for direct uploads to the EncryptedDiskService. It can actually accept
|
19
|
+
# uploads to any encrypted service, but for S3 and GCP the upload can be done to the cloud storage
|
20
|
+
# bucket directly.
|
21
|
+
def update
|
22
|
+
params = read_params_from_token_and_headers_for_put
|
23
|
+
service = lookup_service(params[:service_name])
|
24
|
+
key = params[:key]
|
25
|
+
|
26
|
+
service.upload(key, request.body,
|
27
|
+
content_type: params[:content_type],
|
28
|
+
content_length: params[:content_length],
|
29
|
+
checksum: params[:checksum],
|
30
|
+
encryption_key: params[:encryption_key])
|
31
|
+
rescue InvalidParams, ActiveStorageEncryption::IncorrectEncryptionKey, ActiveSupport::MessageVerifier::InvalidSignature, ActiveStorage::IntegrityError
|
32
|
+
head :unprocessable_entity
|
33
|
+
end
|
34
|
+
|
35
|
+
# Streams the decrypted contents of an encrypted blob
|
36
|
+
def show
|
37
|
+
params = read_params_from_token_and_headers_for_get
|
38
|
+
service = lookup_service(params[:service_name])
|
39
|
+
raise InvalidParams, "#{service.name} does not allow private URLs" if service.private_url_policy == :disable
|
40
|
+
|
41
|
+
key = params[:key]
|
42
|
+
encryption_key = params[:encryption_key]
|
43
|
+
|
44
|
+
send_stream(filename: params[:filename], disposition: params[:disposition] || DEFAULT_BLOB_STREAMING_DISPOSITION, type: params[:content_type]) do |stream|
|
45
|
+
service.download(key, encryption_key: encryption_key) do |chunk|
|
46
|
+
stream.write chunk
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue InvalidParams, ActiveStorageEncryption::StreamingTokenInvalidOrExpired, ActiveSupport::MessageEncryptor::InvalidMessage, ActiveStorageEncryption::IncorrectEncryptionKey
|
50
|
+
head :forbidden
|
51
|
+
end
|
52
|
+
|
53
|
+
# Creates a Blob record with a random encryption key and returns the details for PUTing it
|
54
|
+
# This is only necessary because in Rails there is some disagreement regarding the service_name parameter.
|
55
|
+
# See https://github.com/rails/rails/issues/38940
|
56
|
+
# It does not require the service to support encryption. However, we mandate that the MD5 be provided upfront,
|
57
|
+
# so that it gets included into the signature
|
58
|
+
def create_direct_upload
|
59
|
+
blob_params = params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {})
|
60
|
+
unless blob_params[:checksum]
|
61
|
+
render(plain: "The `checksum' is required", status: :unprocessable_entity) and return
|
62
|
+
end
|
63
|
+
|
64
|
+
service = lookup_service(params.require(:service_name))
|
65
|
+
blob = ActiveStorage::Blob.create_before_direct_upload!(
|
66
|
+
**blob_params.to_h.symbolize_keys,
|
67
|
+
service_name: service.name
|
68
|
+
)
|
69
|
+
render json: direct_upload_json(blob)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def read_params_from_token_and_headers_for_put
|
75
|
+
token_str = params.require(:token)
|
76
|
+
|
77
|
+
# The token params for PUT / direct upload are signed but not encrypted - the encryption key
|
78
|
+
# is transmitted inside headers
|
79
|
+
token_params = ActiveStorage.verifier.verify(token_str, purpose: :encrypted_put).symbolize_keys
|
80
|
+
|
81
|
+
# Ensure we are getting sent exactly as many bytes as stated in the token
|
82
|
+
raise InvalidParams, "Request must specify body content-length" if request.headers["content-length"].blank?
|
83
|
+
|
84
|
+
actual_content_length = request.headers["content-length"].to_i
|
85
|
+
expected_content_length = token_params.fetch(:content_length)
|
86
|
+
if actual_content_length != expected_content_length
|
87
|
+
raise InvalidParams, "content-length mismatch, expecting upload of #{expected_content_length} bytes but sent #{actual_content_length}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Recover the encryption key from the headers (similar to how cloud storage services do it)
|
91
|
+
b64_encryption_key = request.headers["x-active-storage-encryption-key"]
|
92
|
+
raise InvalidParams, "x-active-storage-encryption-key header is missing" if b64_encryption_key.blank?
|
93
|
+
encryption_key = Base64.strict_decode64(b64_encryption_key)
|
94
|
+
|
95
|
+
# Verify the SHA of the encryption key
|
96
|
+
encryption_key_b64sha = Digest::SHA256.base64digest(encryption_key)
|
97
|
+
raise InvalidParams, "Incorrect checksum for the encryption key" unless Rack::Utils.secure_compare(encryption_key_b64sha, token_params.fetch(:encryption_key_sha256))
|
98
|
+
|
99
|
+
# Verify the Content-MD5
|
100
|
+
b64_md5_from_headers = request.headers["content-md5"]
|
101
|
+
raise InvalidParams, "Content-MD5 header is required" if b64_md5_from_headers.blank?
|
102
|
+
raise InvalidParams, "Content-MD5 differs from the known checksum" unless Rack::Utils.secure_compare(b64_md5_from_headers, token_params.fetch(:checksum))
|
103
|
+
|
104
|
+
{
|
105
|
+
key: token_params.fetch(:key),
|
106
|
+
encryption_key: encryption_key,
|
107
|
+
service_name: token_params.fetch(:service_name),
|
108
|
+
checksum: token_params[:checksum],
|
109
|
+
content_type: token_params.fetch(:content_type),
|
110
|
+
content_length: token_params.fetch(:content_length)
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def read_params_from_token_and_headers_for_get
|
115
|
+
token_str = params.require(:token)
|
116
|
+
|
117
|
+
# The token params for GET / private_url download are encrypted, as they contain the object encryption key.
|
118
|
+
token_params = ActiveStorageEncryption.token_encryptor.decrypt_and_verify(token_str, purpose: :encrypted_get).symbolize_keys
|
119
|
+
encryption_key = Base64.decode64(token_params.fetch(:encryption_key))
|
120
|
+
|
121
|
+
service = lookup_service(token_params.fetch(:service_name))
|
122
|
+
|
123
|
+
# To be more like cloud services: verify presence of headers, if we were asked to (but this is optional)
|
124
|
+
if service.private_url_policy == :require_headers
|
125
|
+
b64_encryption_key = request.headers["x-active-storage-encryption-key"]
|
126
|
+
raise InvalidParams, "x-active-storage-encryption-key header is missing" if b64_encryption_key.blank?
|
127
|
+
raise InvalidParams, "Incorrect encryption key supplied via header" unless Rack::Utils.secure_compare(Base64.decode64(b64_encryption_key), encryption_key)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Verify the SHA of the encryption key
|
131
|
+
encryption_key_b64sha = Digest::SHA256.base64digest(encryption_key)
|
132
|
+
raise InvalidParams, "Incorrect encryption key supplied via token" unless Rack::Utils.secure_compare(encryption_key_b64sha, token_params.fetch(:encryption_key_sha256))
|
133
|
+
|
134
|
+
{
|
135
|
+
key: token_params.fetch(:key),
|
136
|
+
encryption_key: encryption_key,
|
137
|
+
service_name: token_params.fetch(:service_name),
|
138
|
+
disposition: token_params.fetch(:disposition),
|
139
|
+
content_type: token_params.fetch(:content_type)
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def lookup_service(name)
|
144
|
+
service = ActiveStorage::Blob.services.fetch(name) { ActiveStorage::Blob.service }
|
145
|
+
raise InvalidParams, "#{service.name} is not providing file encryption" unless service.try(:encrypted?)
|
146
|
+
service
|
147
|
+
end
|
148
|
+
|
149
|
+
def blob_args
|
150
|
+
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, :service_name, metadata: {}).to_h.symbolize_keys
|
151
|
+
end
|
152
|
+
|
153
|
+
def service_name_from_params_or_config
|
154
|
+
params[:service_name] || ActiveStorage::Blob.service.name # ? Rails.application.config.active_storage.service.name
|
155
|
+
end
|
156
|
+
|
157
|
+
def direct_upload_json(blob)
|
158
|
+
blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
|
159
|
+
url: blob.service_url_for_direct_upload,
|
160
|
+
headers: blob.service_headers_for_direct_upload
|
161
|
+
})
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorageEncryption::EncryptedDiskService::V1Scheme
|
4
|
+
def initialize(encryption_key)
|
5
|
+
@scheme = BlockCipherKit::AES256CFBCIVScheme.new(encryption_key)
|
6
|
+
@key_digest = Digest::SHA256.digest(encryption_key.byteslice(0, 16 + 32)) # In this scheme the IV is suffixed with the key
|
7
|
+
end
|
8
|
+
|
9
|
+
def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk)
|
10
|
+
validate_key!(from_ciphertext_io)
|
11
|
+
@scheme.streaming_decrypt(from_ciphertext_io:, into_plaintext_io:, &blk)
|
12
|
+
end
|
13
|
+
|
14
|
+
def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk)
|
15
|
+
into_ciphertext_io.write(@key_digest)
|
16
|
+
@scheme.streaming_encrypt(into_ciphertext_io:, from_plaintext_io:, &blk)
|
17
|
+
end
|
18
|
+
|
19
|
+
def decrypt_range(from_ciphertext_io:, range:)
|
20
|
+
validate_key!(from_ciphertext_io)
|
21
|
+
@scheme.decrypt_range(from_ciphertext_io:, range:)
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_key!(io)
|
25
|
+
key_digest_from_io = io.read(@key_digest.bytesize)
|
26
|
+
raise ActiveStorageEncryption::IncorrectEncryptionKey unless key_digest_from_io == @key_digest
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This scheme uses GCM encryption with CTR-based random access. The auth tag is stored
|
4
|
+
# at the end of the message. The message is prefixed by a SHA2 digest of the encryption key.
|
5
|
+
class ActiveStorageEncryption::EncryptedDiskService::V2Scheme
|
6
|
+
def initialize(encryption_key)
|
7
|
+
@scheme = BlockCipherKit::AES256GCMScheme.new(encryption_key)
|
8
|
+
@key_digest = Digest::SHA256.digest(encryption_key.byteslice(0, 32)) # In this scheme just the key is used
|
9
|
+
end
|
10
|
+
|
11
|
+
def streaming_decrypt(from_ciphertext_io:, into_plaintext_io: nil, &blk)
|
12
|
+
check_key!(from_ciphertext_io)
|
13
|
+
@scheme.streaming_decrypt(from_ciphertext_io:, into_plaintext_io:, &blk)
|
14
|
+
end
|
15
|
+
|
16
|
+
def streaming_encrypt(into_ciphertext_io:, from_plaintext_io: nil, &blk)
|
17
|
+
# See check_key! for rationale. We need a fast KVC (key validation code)
|
18
|
+
# to refuse the download if we know the key is incorrect.
|
19
|
+
into_ciphertext_io.write(@key_digest)
|
20
|
+
@scheme.streaming_encrypt(into_ciphertext_io:, from_plaintext_io:, &blk)
|
21
|
+
end
|
22
|
+
|
23
|
+
def decrypt_range(from_ciphertext_io:, range:)
|
24
|
+
check_key!(from_ciphertext_io)
|
25
|
+
@scheme.decrypt_range(from_ciphertext_io:, range:)
|
26
|
+
end
|
27
|
+
|
28
|
+
private def check_key!(io)
|
29
|
+
# We need a fast KCV (key check value) to refuse the download
|
30
|
+
# if we know the key is incorrect. We can't use the auth tag from GCM
|
31
|
+
# because it can only be computed if the entirety of the ciphertext has been read by the
|
32
|
+
# cipher - and we want random access. We could use a HMAC(encryption_key, auth_tag) at the
|
33
|
+
# tail of ciphertext to achieve the same, but that would require streaming_decrypt to seek inside
|
34
|
+
# the ciphertext IO to read the tail of the file - which we don't want to require.
|
35
|
+
#
|
36
|
+
# Besides, we want to not tie up server resources if we know
|
37
|
+
# that the furnished encryption key is incorrect. So: a KVC.
|
38
|
+
#
|
39
|
+
# We store the SHA2 value of the encryption key at the start of the ciphertext. We assume that the encryption
|
40
|
+
# key will be generated randomly and will be very high-entropy, so the only attack strategy for it is brute-force.
|
41
|
+
# Brute-force is keyspace / hashrate, as explained here: https://stackoverflow.com/questions/4764026/how-many-sha256-hashes-can-a-modern-computer-compute
|
42
|
+
# which, for our key of 32 bytes, gives us this calculation to find out the number of years to crack this SHA on
|
43
|
+
# a GeForce 2080Ti (based on https://hashcat.net/forum/thread-10185.html):
|
44
|
+
# ((256 ** 32) / (7173 * 1000 * 1000)) / 60 / 60 / 24 / 365
|
45
|
+
# which is
|
46
|
+
# 511883878862512581460395486615240253212171357229849212045742
|
47
|
+
# This is quite some years. So storing the digest of the key is reasonably safe.
|
48
|
+
key_digest_from_io = io.read(@key_digest.bytesize)
|
49
|
+
raise ActiveStorageEncryption::IncorrectEncryptionKey unless key_digest_from_io == @key_digest
|
50
|
+
end
|
51
|
+
end
|