active_storage_encryption 0.2.2 → 0.3.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/active_storage_encryption.gemspec +2 -0
- data/gemfiles/rails_7.gemfile.lock +77 -1
- data/gemfiles/rails_8.gemfile.lock +77 -1
- data/lib/active_storage/service/encrypted_gcs_service.rb +10 -0
- data/lib/active_storage_encryption/encrypted_blob_proxy_controller.rb +1 -1
- data/lib/active_storage_encryption/encrypted_gcs_service.rb +158 -0
- data/lib/active_storage_encryption/overrides.rb +33 -5
- data/lib/active_storage_encryption/private_url_policy.rb +1 -1
- data/lib/active_storage_encryption/version.rb +1 -1
- data/lib/active_storage_encryption.rb +1 -0
- data/test/dummy/app/models/user.rb +5 -0
- data/test/dummy/config/environments/test.rb +2 -0
- data/test/dummy/config/storage.yml +3 -0
- data/test/dummy/db/migrate/20250428093315_create_users.rb +7 -0
- data/test/dummy/db/schema.rb +6 -3
- data/test/lib/encrypted_gcs_service_test.rb +201 -0
- data/test/lib/encrypted_s3_service_test.rb +4 -1
- data/test/lib/overrides_test.rb +349 -0
- metadata +36 -3
- data/test/fixtures/files/.keep +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f34184e53ab51b15143acc6fd7bbc2ea0e43a4cc4c0b0d87c065de2631298fb
|
4
|
+
data.tar.gz: 8329f26f7141a0762bb0488995bdf219ff01c00d9b374b4cdecc7af895bded11
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30d7ba6406ec77a7521cc47ea24b858e0de4502644516c3ea5a54b025e24ffe305b21f4fcf3244f1361c2fbedcc79f8dc8b8ce6a0e0e3558a202e1e21941a295
|
7
|
+
data.tar.gz: 71bf3a1c8ee30daee4bdb43bd8703b2c530e67fb0e68078fc07d208e266d1285c8f390392e6539afaaea839c3fa62e2b2cef46b8a863c4d57409d1ebe399c392
|
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
|
|
35
35
|
# Testing with cloud services
|
36
36
|
spec.add_development_dependency "aws-sdk-s3"
|
37
37
|
spec.add_development_dependency "net-http"
|
38
|
+
spec.add_development_dependency "google-cloud-storage"
|
38
39
|
|
39
40
|
# Code formatting, linting and testing
|
40
41
|
spec.add_development_dependency "sqlite3"
|
@@ -42,4 +43,5 @@ Gem::Specification.new do |spec|
|
|
42
43
|
spec.add_development_dependency "appraisal"
|
43
44
|
spec.add_development_dependency "magic_frozen_string_literal"
|
44
45
|
spec.add_development_dependency "rake"
|
46
|
+
spec.add_development_dependency "pry"
|
45
47
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
active_storage_encryption (0.
|
4
|
+
active_storage_encryption (0.3.0)
|
5
5
|
activestorage
|
6
6
|
block_cipher_kit (>= 0.0.4)
|
7
7
|
rails (>= 7.2.2.1)
|
@@ -81,6 +81,8 @@ GEM
|
|
81
81
|
minitest (>= 5.1)
|
82
82
|
securerandom (>= 0.3)
|
83
83
|
tzinfo (~> 2.0, >= 2.0.5)
|
84
|
+
addressable (2.8.7)
|
85
|
+
public_suffix (>= 2.0.2, < 7.0)
|
84
86
|
appraisal (2.5.0)
|
85
87
|
bundler
|
86
88
|
rake
|
@@ -108,14 +110,63 @@ GEM
|
|
108
110
|
bigdecimal (3.1.9)
|
109
111
|
block_cipher_kit (0.0.4)
|
110
112
|
builder (3.3.0)
|
113
|
+
coderay (1.1.3)
|
111
114
|
concurrent-ruby (1.3.5)
|
112
115
|
connection_pool (2.5.0)
|
113
116
|
crass (1.0.6)
|
114
117
|
date (3.4.1)
|
118
|
+
declarative (0.0.20)
|
119
|
+
digest-crc (0.7.0)
|
120
|
+
rake (>= 12.0.0, < 14.0.0)
|
115
121
|
drb (2.2.1)
|
116
122
|
erubi (1.13.1)
|
123
|
+
faraday (2.13.0)
|
124
|
+
faraday-net_http (>= 2.0, < 3.5)
|
125
|
+
json
|
126
|
+
logger
|
127
|
+
faraday-net_http (3.4.0)
|
128
|
+
net-http (>= 0.5.0)
|
117
129
|
globalid (1.2.1)
|
118
130
|
activesupport (>= 6.1)
|
131
|
+
google-apis-core (0.16.0)
|
132
|
+
addressable (~> 2.5, >= 2.5.1)
|
133
|
+
googleauth (~> 1.9)
|
134
|
+
httpclient (>= 2.8.3, < 3.a)
|
135
|
+
mini_mime (~> 1.0)
|
136
|
+
mutex_m
|
137
|
+
representable (~> 3.0)
|
138
|
+
retriable (>= 2.0, < 4.a)
|
139
|
+
google-apis-iamcredentials_v1 (0.22.0)
|
140
|
+
google-apis-core (>= 0.15.0, < 2.a)
|
141
|
+
google-apis-storage_v1 (0.50.0)
|
142
|
+
google-apis-core (>= 0.15.0, < 2.a)
|
143
|
+
google-cloud-core (1.8.0)
|
144
|
+
google-cloud-env (>= 1.0, < 3.a)
|
145
|
+
google-cloud-errors (~> 1.0)
|
146
|
+
google-cloud-env (2.2.2)
|
147
|
+
base64 (~> 0.2)
|
148
|
+
faraday (>= 1.0, < 3.a)
|
149
|
+
google-cloud-errors (1.5.0)
|
150
|
+
google-cloud-storage (1.56.0)
|
151
|
+
addressable (~> 2.8)
|
152
|
+
digest-crc (~> 0.4)
|
153
|
+
google-apis-core (~> 0.13)
|
154
|
+
google-apis-iamcredentials_v1 (~> 0.18)
|
155
|
+
google-apis-storage_v1 (>= 0.42)
|
156
|
+
google-cloud-core (~> 1.6)
|
157
|
+
googleauth (~> 1.9)
|
158
|
+
mini_mime (~> 1.0)
|
159
|
+
google-logging-utils (0.1.0)
|
160
|
+
googleauth (1.14.0)
|
161
|
+
faraday (>= 1.0, < 3.a)
|
162
|
+
google-cloud-env (~> 2.2)
|
163
|
+
google-logging-utils (~> 0.1)
|
164
|
+
jwt (>= 1.4, < 3.0)
|
165
|
+
multi_json (~> 1.11)
|
166
|
+
os (>= 0.9, < 2.0)
|
167
|
+
signet (>= 0.16, < 2.a)
|
168
|
+
httpclient (2.9.0)
|
169
|
+
mutex_m
|
119
170
|
i18n (1.14.7)
|
120
171
|
concurrent-ruby (~> 1.0)
|
121
172
|
io-console (0.8.0)
|
@@ -125,6 +176,8 @@ GEM
|
|
125
176
|
reline (>= 0.4.2)
|
126
177
|
jmespath (1.6.2)
|
127
178
|
json (2.10.1)
|
179
|
+
jwt (2.10.1)
|
180
|
+
base64
|
128
181
|
language_server-protocol (3.17.0.4)
|
129
182
|
lint_roller (1.1.0)
|
130
183
|
logger (1.6.6)
|
@@ -138,8 +191,11 @@ GEM
|
|
138
191
|
net-pop
|
139
192
|
net-smtp
|
140
193
|
marcel (1.0.4)
|
194
|
+
method_source (1.1.0)
|
141
195
|
mini_mime (1.1.5)
|
142
196
|
minitest (5.25.4)
|
197
|
+
multi_json (1.15.0)
|
198
|
+
mutex_m (0.3.0)
|
143
199
|
net-http (0.6.0)
|
144
200
|
uri
|
145
201
|
net-imap (0.5.6)
|
@@ -158,6 +214,7 @@ GEM
|
|
158
214
|
racc (~> 1.4)
|
159
215
|
nokogiri (1.18.3-x86_64-linux-gnu)
|
160
216
|
racc (~> 1.4)
|
217
|
+
os (1.1.4)
|
161
218
|
parallel (1.26.3)
|
162
219
|
parser (3.3.7.1)
|
163
220
|
ast (~> 2.4.1)
|
@@ -165,9 +222,13 @@ GEM
|
|
165
222
|
pp (0.6.2)
|
166
223
|
prettyprint
|
167
224
|
prettyprint (0.2.0)
|
225
|
+
pry (0.15.2)
|
226
|
+
coderay (~> 1.1)
|
227
|
+
method_source (~> 1.0)
|
168
228
|
psych (5.2.3)
|
169
229
|
date
|
170
230
|
stringio
|
231
|
+
public_suffix (6.0.1)
|
171
232
|
racc (1.8.1)
|
172
233
|
rack (3.1.11)
|
173
234
|
rack-session (2.1.0)
|
@@ -213,6 +274,11 @@ GEM
|
|
213
274
|
regexp_parser (2.10.0)
|
214
275
|
reline (0.6.0)
|
215
276
|
io-console (~> 0.5)
|
277
|
+
representable (3.2.0)
|
278
|
+
declarative (< 0.1.0)
|
279
|
+
trailblazer-option (>= 0.1.1, < 0.2.0)
|
280
|
+
uber (< 0.2.0)
|
281
|
+
retriable (3.1.2)
|
216
282
|
rubocop (1.71.2)
|
217
283
|
json (~> 2.3)
|
218
284
|
language_server-protocol (>= 3.17.0)
|
@@ -232,6 +298,11 @@ GEM
|
|
232
298
|
securerandom (0.4.1)
|
233
299
|
serve_byte_range (1.0.0)
|
234
300
|
rack (>= 1.0)
|
301
|
+
signet (0.19.0)
|
302
|
+
addressable (~> 2.8)
|
303
|
+
faraday (>= 0.17.5, < 3.a)
|
304
|
+
jwt (>= 1.5, < 3.0)
|
305
|
+
multi_json (~> 1.10)
|
235
306
|
sqlite3 (2.6.0-arm64-darwin)
|
236
307
|
sqlite3 (2.6.0-x86_64-darwin)
|
237
308
|
sqlite3 (2.6.0-x86_64-linux-gnu)
|
@@ -250,8 +321,10 @@ GEM
|
|
250
321
|
stringio (3.1.5)
|
251
322
|
thor (1.3.2)
|
252
323
|
timeout (0.4.3)
|
324
|
+
trailblazer-option (0.1.2)
|
253
325
|
tzinfo (2.0.6)
|
254
326
|
concurrent-ruby (~> 1.0)
|
327
|
+
uber (0.1.0)
|
255
328
|
unicode-display_width (3.1.4)
|
256
329
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
257
330
|
unicode-emoji (4.0.4)
|
@@ -265,6 +338,7 @@ GEM
|
|
265
338
|
|
266
339
|
PLATFORMS
|
267
340
|
arm64-darwin-21
|
341
|
+
arm64-darwin-23
|
268
342
|
arm64-darwin-24
|
269
343
|
x86_64-darwin
|
270
344
|
x86_64-linux
|
@@ -273,8 +347,10 @@ DEPENDENCIES
|
|
273
347
|
active_storage_encryption!
|
274
348
|
appraisal
|
275
349
|
aws-sdk-s3
|
350
|
+
google-cloud-storage
|
276
351
|
magic_frozen_string_literal
|
277
352
|
net-http
|
353
|
+
pry
|
278
354
|
rails (< 8.0)
|
279
355
|
rake
|
280
356
|
sqlite3
|
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
active_storage_encryption (0.
|
4
|
+
active_storage_encryption (0.3.0)
|
5
5
|
activestorage
|
6
6
|
block_cipher_kit (>= 0.0.4)
|
7
7
|
rails (>= 7.2.2.1)
|
@@ -81,6 +81,8 @@ GEM
|
|
81
81
|
securerandom (>= 0.3)
|
82
82
|
tzinfo (~> 2.0, >= 2.0.5)
|
83
83
|
uri (>= 0.13.1)
|
84
|
+
addressable (2.8.7)
|
85
|
+
public_suffix (>= 2.0.2, < 7.0)
|
84
86
|
appraisal (2.5.0)
|
85
87
|
bundler
|
86
88
|
rake
|
@@ -108,14 +110,63 @@ GEM
|
|
108
110
|
bigdecimal (3.1.9)
|
109
111
|
block_cipher_kit (0.0.4)
|
110
112
|
builder (3.3.0)
|
113
|
+
coderay (1.1.3)
|
111
114
|
concurrent-ruby (1.3.5)
|
112
115
|
connection_pool (2.5.0)
|
113
116
|
crass (1.0.6)
|
114
117
|
date (3.4.1)
|
118
|
+
declarative (0.0.20)
|
119
|
+
digest-crc (0.7.0)
|
120
|
+
rake (>= 12.0.0, < 14.0.0)
|
115
121
|
drb (2.2.1)
|
116
122
|
erubi (1.13.1)
|
123
|
+
faraday (2.13.0)
|
124
|
+
faraday-net_http (>= 2.0, < 3.5)
|
125
|
+
json
|
126
|
+
logger
|
127
|
+
faraday-net_http (3.4.0)
|
128
|
+
net-http (>= 0.5.0)
|
117
129
|
globalid (1.2.1)
|
118
130
|
activesupport (>= 6.1)
|
131
|
+
google-apis-core (0.16.0)
|
132
|
+
addressable (~> 2.5, >= 2.5.1)
|
133
|
+
googleauth (~> 1.9)
|
134
|
+
httpclient (>= 2.8.3, < 3.a)
|
135
|
+
mini_mime (~> 1.0)
|
136
|
+
mutex_m
|
137
|
+
representable (~> 3.0)
|
138
|
+
retriable (>= 2.0, < 4.a)
|
139
|
+
google-apis-iamcredentials_v1 (0.22.0)
|
140
|
+
google-apis-core (>= 0.15.0, < 2.a)
|
141
|
+
google-apis-storage_v1 (0.50.0)
|
142
|
+
google-apis-core (>= 0.15.0, < 2.a)
|
143
|
+
google-cloud-core (1.8.0)
|
144
|
+
google-cloud-env (>= 1.0, < 3.a)
|
145
|
+
google-cloud-errors (~> 1.0)
|
146
|
+
google-cloud-env (2.2.2)
|
147
|
+
base64 (~> 0.2)
|
148
|
+
faraday (>= 1.0, < 3.a)
|
149
|
+
google-cloud-errors (1.5.0)
|
150
|
+
google-cloud-storage (1.56.0)
|
151
|
+
addressable (~> 2.8)
|
152
|
+
digest-crc (~> 0.4)
|
153
|
+
google-apis-core (~> 0.13)
|
154
|
+
google-apis-iamcredentials_v1 (~> 0.18)
|
155
|
+
google-apis-storage_v1 (>= 0.42)
|
156
|
+
google-cloud-core (~> 1.6)
|
157
|
+
googleauth (~> 1.9)
|
158
|
+
mini_mime (~> 1.0)
|
159
|
+
google-logging-utils (0.1.0)
|
160
|
+
googleauth (1.14.0)
|
161
|
+
faraday (>= 1.0, < 3.a)
|
162
|
+
google-cloud-env (~> 2.2)
|
163
|
+
google-logging-utils (~> 0.1)
|
164
|
+
jwt (>= 1.4, < 3.0)
|
165
|
+
multi_json (~> 1.11)
|
166
|
+
os (>= 0.9, < 2.0)
|
167
|
+
signet (>= 0.16, < 2.a)
|
168
|
+
httpclient (2.9.0)
|
169
|
+
mutex_m
|
119
170
|
i18n (1.14.7)
|
120
171
|
concurrent-ruby (~> 1.0)
|
121
172
|
io-console (0.8.0)
|
@@ -125,6 +176,8 @@ GEM
|
|
125
176
|
reline (>= 0.4.2)
|
126
177
|
jmespath (1.6.2)
|
127
178
|
json (2.10.1)
|
179
|
+
jwt (2.10.1)
|
180
|
+
base64
|
128
181
|
language_server-protocol (3.17.0.4)
|
129
182
|
lint_roller (1.1.0)
|
130
183
|
logger (1.6.6)
|
@@ -138,8 +191,11 @@ GEM
|
|
138
191
|
net-pop
|
139
192
|
net-smtp
|
140
193
|
marcel (1.0.4)
|
194
|
+
method_source (1.1.0)
|
141
195
|
mini_mime (1.1.5)
|
142
196
|
minitest (5.25.4)
|
197
|
+
multi_json (1.15.0)
|
198
|
+
mutex_m (0.3.0)
|
143
199
|
net-http (0.6.0)
|
144
200
|
uri
|
145
201
|
net-imap (0.5.6)
|
@@ -158,6 +214,7 @@ GEM
|
|
158
214
|
racc (~> 1.4)
|
159
215
|
nokogiri (1.18.3-x86_64-linux-gnu)
|
160
216
|
racc (~> 1.4)
|
217
|
+
os (1.1.4)
|
161
218
|
parallel (1.26.3)
|
162
219
|
parser (3.3.7.1)
|
163
220
|
ast (~> 2.4.1)
|
@@ -165,9 +222,13 @@ GEM
|
|
165
222
|
pp (0.6.2)
|
166
223
|
prettyprint
|
167
224
|
prettyprint (0.2.0)
|
225
|
+
pry (0.15.2)
|
226
|
+
coderay (~> 1.1)
|
227
|
+
method_source (~> 1.0)
|
168
228
|
psych (5.2.3)
|
169
229
|
date
|
170
230
|
stringio
|
231
|
+
public_suffix (6.0.1)
|
171
232
|
racc (1.8.1)
|
172
233
|
rack (3.1.11)
|
173
234
|
rack-session (2.1.0)
|
@@ -213,6 +274,11 @@ GEM
|
|
213
274
|
regexp_parser (2.10.0)
|
214
275
|
reline (0.6.0)
|
215
276
|
io-console (~> 0.5)
|
277
|
+
representable (3.2.0)
|
278
|
+
declarative (< 0.1.0)
|
279
|
+
trailblazer-option (>= 0.1.1, < 0.2.0)
|
280
|
+
uber (< 0.2.0)
|
281
|
+
retriable (3.1.2)
|
216
282
|
rubocop (1.71.2)
|
217
283
|
json (~> 2.3)
|
218
284
|
language_server-protocol (>= 3.17.0)
|
@@ -232,6 +298,11 @@ GEM
|
|
232
298
|
securerandom (0.4.1)
|
233
299
|
serve_byte_range (1.0.0)
|
234
300
|
rack (>= 1.0)
|
301
|
+
signet (0.19.0)
|
302
|
+
addressable (~> 2.8)
|
303
|
+
faraday (>= 0.17.5, < 3.a)
|
304
|
+
jwt (>= 1.5, < 3.0)
|
305
|
+
multi_json (~> 1.10)
|
235
306
|
sqlite3 (2.6.0-arm64-darwin)
|
236
307
|
sqlite3 (2.6.0-x86_64-darwin)
|
237
308
|
sqlite3 (2.6.0-x86_64-linux-gnu)
|
@@ -250,8 +321,10 @@ GEM
|
|
250
321
|
stringio (3.1.5)
|
251
322
|
thor (1.3.2)
|
252
323
|
timeout (0.4.3)
|
324
|
+
trailblazer-option (0.1.2)
|
253
325
|
tzinfo (2.0.6)
|
254
326
|
concurrent-ruby (~> 1.0)
|
327
|
+
uber (0.1.0)
|
255
328
|
unicode-display_width (3.1.4)
|
256
329
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
257
330
|
unicode-emoji (4.0.4)
|
@@ -265,6 +338,7 @@ GEM
|
|
265
338
|
|
266
339
|
PLATFORMS
|
267
340
|
arm64-darwin-21
|
341
|
+
arm64-darwin-23
|
268
342
|
arm64-darwin-24
|
269
343
|
x86_64-darwin
|
270
344
|
x86_64-linux
|
@@ -273,8 +347,10 @@ DEPENDENCIES
|
|
273
347
|
active_storage_encryption!
|
274
348
|
appraisal
|
275
349
|
aws-sdk-s3
|
350
|
+
google-cloud-storage
|
276
351
|
magic_frozen_string_literal
|
277
352
|
net-http
|
353
|
+
pry
|
278
354
|
rails (>= 8.0)
|
279
355
|
rake
|
280
356
|
sqlite3
|
@@ -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::EncryptedGCSService < ActiveStorageEncryption::EncryptedGCSService
|
10
|
+
end
|
@@ -105,7 +105,7 @@ class ActiveStorageEncryption::EncryptedBlobProxyController < ActionController::
|
|
105
105
|
blob_etag = key.inspect # Strong ETags must be quoted
|
106
106
|
status, headers, ranges_body = ServeByteRange.serve_ranges(request.env,
|
107
107
|
resource_size: blob_byte_size,
|
108
|
-
etag: blob_etag,
|
108
|
+
etag: blob_etag,
|
109
109
|
resource_content_type: type,
|
110
110
|
&streaming_proc)
|
111
111
|
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_storage/service/gcs_service"
|
4
|
+
require "google/cloud/storage/service"
|
5
|
+
|
6
|
+
class ActiveStorageEncryption::EncryptedGCSService < ActiveStorage::Service::GCSService
|
7
|
+
include ActiveStorageEncryption::PrivateUrlPolicy
|
8
|
+
GCS_ENCRYPTION_KEY_LENGTH_BYTES = 32 # google wants to get a 32 byte key
|
9
|
+
|
10
|
+
def encrypted? = true
|
11
|
+
|
12
|
+
def public? = false
|
13
|
+
|
14
|
+
def service_name
|
15
|
+
# ActiveStorage::Service::DiskService => Disk
|
16
|
+
# Overridden because in Rails 8 this is "self.class.name.split("::").third.remove("Service")"
|
17
|
+
self.class.name.split("::").last.remove("Service")
|
18
|
+
end
|
19
|
+
|
20
|
+
def upload(key, io, encryption_key: nil, checksum: nil, content_type: nil, disposition: nil, filename: nil, custom_metadata: {})
|
21
|
+
instrument :upload, key: key, checksum: checksum do
|
22
|
+
# GCS's signed URLs don't include params such as response-content-type response-content_disposition
|
23
|
+
# in the signature, which means an attacker can modify them and bypass our effort to force these to
|
24
|
+
# binary and attachment when the file's content type requires it. The only way to force them is to
|
25
|
+
# store them as object's metadata.
|
26
|
+
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
|
27
|
+
bucket.create_file(io, key, md5: checksum, cache_control: @config[:cache_control], content_type: content_type, content_disposition: content_disposition, metadata: custom_metadata, encryption_key: derive_service_encryption_key(encryption_key))
|
28
|
+
rescue Google::Cloud::InvalidArgumentError => e
|
29
|
+
raise ActiveStorage::IntegrityError, e
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def url_for_direct_upload(key, expires_in:, checksum:, encryption_key:, content_type: nil, custom_metadata: {}, filename: nil, **)
|
34
|
+
instrument :url, key: key do |payload|
|
35
|
+
headers = headers_for_direct_upload(key, checksum:, encryption_key:, content_type:, filename:, custom_metadata:)
|
36
|
+
|
37
|
+
version = :v4
|
38
|
+
|
39
|
+
args = {
|
40
|
+
content_md5: checksum,
|
41
|
+
expires: expires_in,
|
42
|
+
headers: headers,
|
43
|
+
method: "PUT",
|
44
|
+
version: version
|
45
|
+
}
|
46
|
+
|
47
|
+
if @config[:iam]
|
48
|
+
args[:issuer] = issuer
|
49
|
+
args[:signer] = signer
|
50
|
+
end
|
51
|
+
|
52
|
+
generated_url = bucket.signed_url(key, **args)
|
53
|
+
|
54
|
+
payload[:url] = generated_url
|
55
|
+
|
56
|
+
generated_url
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def headers_for_direct_upload(key, checksum:, encryption_key:, filename: nil, disposition: nil, content_type: nil, custom_metadata: {}, **)
|
61
|
+
headers = {
|
62
|
+
"Content-Type" => content_type,
|
63
|
+
"Content-MD5" => checksum, # Not strictly required, but it ensures the file bytes we upload match what we want. This way google will error when we upload garbage.
|
64
|
+
**gcs_encryption_key_headers(derive_service_encryption_key(encryption_key)),
|
65
|
+
**custom_metadata_headers(custom_metadata)
|
66
|
+
}
|
67
|
+
headers["Content-Disposition"] = content_disposition_with(type: disposition, filename: filename) if filename
|
68
|
+
|
69
|
+
if @config[:cache_control].present?
|
70
|
+
headers["Cache-Control"] = @config[:cache_control]
|
71
|
+
end
|
72
|
+
headers
|
73
|
+
end
|
74
|
+
|
75
|
+
def download(key, encryption_key: nil, &block)
|
76
|
+
if block_given?
|
77
|
+
instrument :streaming_download, key: key do
|
78
|
+
stream(key, encryption_key: encryption_key, &block)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
instrument :download, key: key do
|
82
|
+
file_for(key).download(encryption_key: derive_service_encryption_key(encryption_key)).string
|
83
|
+
rescue Google::Cloud::NotFoundError => e
|
84
|
+
raise ActiveStorage::FileNotFoundError, e
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def download_chunk(key, range, encryption_key: nil)
|
90
|
+
instrument :download_chunk, key: key, range: range do
|
91
|
+
file_for(key).download(range: range, encryption_key: derive_service_encryption_key(encryption_key)).string
|
92
|
+
rescue Google::Cloud::NotFoundError => e
|
93
|
+
raise ActiveStorage::FileNotFoundError, e
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Reads the file for the given key in chunks, yielding each to the block.
|
98
|
+
def stream(key, encryption_key: nil)
|
99
|
+
file = file_for(key, skip_lookup: false)
|
100
|
+
|
101
|
+
chunk_size = 5.megabytes
|
102
|
+
offset = 0
|
103
|
+
|
104
|
+
raise ActiveStorage::FileNotFoundError unless file.present?
|
105
|
+
|
106
|
+
while offset < file.size
|
107
|
+
yield file.download(range: offset..(offset + chunk_size - 1), encryption_key: derive_service_encryption_key(encryption_key)).string
|
108
|
+
offset += chunk_size
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def compose(source_keys, destination_key, encryption_key:, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
|
113
|
+
# Because we will always have a different encryption_key on a blob when created and google requires us to have the same encryption_keys on all source blobs
|
114
|
+
# we need to work this out a bit more. For now we don't need this and thus won't support it in this service.
|
115
|
+
raise NotImplementedError, "Currently composing files is not supported"
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def private_url(key, expires_in:, filename:, content_type:, disposition:, encryption_key:, **remaining_options_for_streaming_url)
|
121
|
+
if private_url_policy == :require_headers
|
122
|
+
args = {
|
123
|
+
expires: expires_in,
|
124
|
+
query: {
|
125
|
+
"response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
|
126
|
+
"response-content-type" => content_type
|
127
|
+
},
|
128
|
+
headers: gcs_encryption_key_headers(derive_service_encryption_key(encryption_key))
|
129
|
+
}
|
130
|
+
|
131
|
+
if @config[:iam]
|
132
|
+
args[:issuer] = issuer
|
133
|
+
args[:signer] = signer
|
134
|
+
end
|
135
|
+
|
136
|
+
file_for(key).signed_url(**args, version: :v4)
|
137
|
+
else
|
138
|
+
private_url_for_streaming_via_controller(key, expires_in:, filename:, content_type:, disposition:, encryption_key:, **remaining_options_for_streaming_url)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def public_url(key, filename:, encryption_key:, content_type: nil, disposition: :inline, **)
|
143
|
+
raise "Public URL's are disabled for this service"
|
144
|
+
end
|
145
|
+
|
146
|
+
def gcs_encryption_key_headers(key)
|
147
|
+
{
|
148
|
+
"x-goog-encryption-algorithm" => "AES256",
|
149
|
+
"x-goog-encryption-key" => Base64.strict_encode64(key),
|
150
|
+
"x-goog-encryption-key-sha256" => Digest::SHA256.base64digest(key)
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def derive_service_encryption_key(blob_encryption_key)
|
155
|
+
raise ArgumentError, "The blob encryption_key must be at least #{GCS_ENCRYPTION_KEY_LENGTH_BYTES} bytes long" unless blob_encryption_key.bytesize >= GCS_ENCRYPTION_KEY_LENGTH_BYTES
|
156
|
+
blob_encryption_key[0...GCS_ENCRYPTION_KEY_LENGTH_BYTES]
|
157
|
+
end
|
158
|
+
end
|
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
module ActiveStorageEncryption
|
4
4
|
module Overrides
|
5
|
+
class EncryptionKeyMissingError < StandardError
|
6
|
+
end
|
7
|
+
|
5
8
|
module EncryptedBlobClassMethods
|
6
9
|
def self.included base
|
7
10
|
base.class_eval do
|
@@ -54,7 +57,7 @@ module ActiveStorageEncryption
|
|
54
57
|
content_type ||= blobs.pluck(:content_type).compact.first
|
55
58
|
|
56
59
|
new(key: key, filename: filename, content_type: content_type, metadata: metadata, byte_size: blobs.sum(&:byte_size), service_name:, encryption_key:).tap do |combined_blob|
|
57
|
-
combined_blob.compose(blobs.pluck(:key))
|
60
|
+
combined_blob.compose(blobs.pluck(:key), source_encryption_keys: blobs.pluck(:encryption_key))
|
58
61
|
combined_blob.save!
|
59
62
|
end
|
60
63
|
end
|
@@ -70,7 +73,8 @@ module ActiveStorageEncryption
|
|
70
73
|
|
71
74
|
def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
|
72
75
|
if service_encrypted?
|
73
|
-
|
76
|
+
ensure_encryption_key_set!
|
77
|
+
|
74
78
|
service.url_for_direct_upload(key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata, encryption_key: encryption_key)
|
75
79
|
else
|
76
80
|
super
|
@@ -78,6 +82,8 @@ module ActiveStorageEncryption
|
|
78
82
|
end
|
79
83
|
|
80
84
|
def open(tmpdir: nil, &block)
|
85
|
+
ensure_encryption_key_set! if service_encrypted?
|
86
|
+
|
81
87
|
service.open(
|
82
88
|
key,
|
83
89
|
encryption_key: encryption_key,
|
@@ -91,6 +97,8 @@ module ActiveStorageEncryption
|
|
91
97
|
|
92
98
|
def service_headers_for_direct_upload
|
93
99
|
if service_encrypted?
|
100
|
+
ensure_encryption_key_set!
|
101
|
+
|
94
102
|
service.headers_for_direct_upload(key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata, encryption_key: encryption_key)
|
95
103
|
else
|
96
104
|
super
|
@@ -99,6 +107,8 @@ module ActiveStorageEncryption
|
|
99
107
|
|
100
108
|
def upload_without_unfurling(io)
|
101
109
|
if service_encrypted?
|
110
|
+
ensure_encryption_key_set!
|
111
|
+
|
102
112
|
service.upload(key, io, checksum: checksum, encryption_key: encryption_key, **service_metadata)
|
103
113
|
else
|
104
114
|
super
|
@@ -109,6 +119,8 @@ module ActiveStorageEncryption
|
|
109
119
|
# That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
|
110
120
|
def download(&block)
|
111
121
|
if service_encrypted?
|
122
|
+
ensure_encryption_key_set!
|
123
|
+
|
112
124
|
service.download(key, encryption_key: encryption_key, &block)
|
113
125
|
else
|
114
126
|
super
|
@@ -117,23 +129,29 @@ module ActiveStorageEncryption
|
|
117
129
|
|
118
130
|
def download_chunk(range)
|
119
131
|
if service_encrypted?
|
132
|
+
ensure_encryption_key_set!
|
133
|
+
|
120
134
|
service.download_chunk(key, range, encryption_key: encryption_key)
|
121
135
|
else
|
122
136
|
super
|
123
137
|
end
|
124
138
|
end
|
125
139
|
|
126
|
-
def compose(keys)
|
140
|
+
def compose(keys, source_encryption_keys: [])
|
127
141
|
if service_encrypted?
|
142
|
+
ensure_encryption_key_set!
|
143
|
+
|
128
144
|
self.composed = true
|
129
|
-
service.compose(keys, key, encryption_key: encryption_key, **service_metadata)
|
145
|
+
service.compose(keys, key, encryption_key: encryption_key, source_encryption_keys: source_encryption_keys, **service_metadata)
|
130
146
|
else
|
131
|
-
super
|
147
|
+
super(keys)
|
132
148
|
end
|
133
149
|
end
|
134
150
|
|
135
151
|
def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
|
136
152
|
if service_encrypted?
|
153
|
+
ensure_encryption_key_set!
|
154
|
+
|
137
155
|
service.url(
|
138
156
|
key, expires_in: expires_in, filename: ActiveStorage::Filename.wrap(filename || self.filename),
|
139
157
|
encryption_key: encryption_key,
|
@@ -156,6 +174,12 @@ module ActiveStorageEncryption
|
|
156
174
|
end
|
157
175
|
super
|
158
176
|
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def ensure_encryption_key_set!
|
181
|
+
raise EncryptionKeyMissingError, "Encryption key must be present" unless encryption_key.present?
|
182
|
+
end
|
159
183
|
end
|
160
184
|
|
161
185
|
module BlobIdentifiableInstanceMethods
|
@@ -178,6 +202,8 @@ module ActiveStorageEncryption
|
|
178
202
|
|
179
203
|
module DownloaderInstanceMethods
|
180
204
|
def open(key, encryption_key: nil, checksum: nil, verify: true, name: "ActiveStorage-", tmpdir: nil, &blk)
|
205
|
+
raise EncryptionKeyMissingError, "An encryption key must be supplied when using an encrypted service" if !encryption_key && service.respond_to?(:encrypted?) && service.encrypted?
|
206
|
+
|
181
207
|
open_tempfile(name, tmpdir) do |file|
|
182
208
|
download(key, file, encryption_key: encryption_key)
|
183
209
|
verify_integrity_of(file, checksum: checksum) if verify
|
@@ -189,6 +215,8 @@ module ActiveStorageEncryption
|
|
189
215
|
|
190
216
|
def download(key, file, encryption_key: nil)
|
191
217
|
if service.respond_to?(:encrypted?) && service.encrypted?
|
218
|
+
raise "An encryption key must be supplied when using an encrypted service" unless encryption_key
|
219
|
+
|
192
220
|
file.binmode
|
193
221
|
service.download(key, encryption_key: encryption_key) { |chunk| file.write(chunk) }
|
194
222
|
file.flush
|
@@ -18,7 +18,7 @@ module ActiveStorageEncryption::PrivateUrlPolicy
|
|
18
18
|
@private_url_policy
|
19
19
|
end
|
20
20
|
|
21
|
-
def private_url_for_streaming_via_controller(key,
|
21
|
+
def private_url_for_streaming_via_controller(key, expires_in:, filename:, content_type:, disposition:, encryption_key:, blob_byte_size:)
|
22
22
|
if private_url_policy == :disable
|
23
23
|
raise ActiveStorageEncryption::StreamingDisabled, <<~EOS
|
24
24
|
Requested a signed GET URL for #{key.inspect} on service #{name}. This service
|