active_storage_encryption 0.1.0 → 0.2.2
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/.github/dependabot.yml +12 -0
- data/.github/workflows/ci.yml +75 -0
- data/.gitignore +14 -0
- data/.ruby-version +1 -0
- data/.standard.yml +1 -0
- data/Appraisals +2 -0
- data/Gemfile +9 -0
- data/README.md +24 -3
- data/Rakefile +0 -2
- data/active_storage_encryption.gemspec +45 -0
- data/config/routes.rb +1 -1
- data/gemfiles/rails_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +10 -1
- data/gemfiles/rails_8.gemfile +1 -0
- data/gemfiles/rails_8.gemfile.lock +10 -1
- data/lib/active_storage_encryption/encrypted_blob_proxy_controller.rb +116 -0
- data/lib/active_storage_encryption/encrypted_blobs_controller.rb +0 -51
- data/lib/active_storage_encryption/encrypted_s3_service.rb +1 -0
- data/lib/active_storage_encryption/engine.rb +4 -0
- data/lib/active_storage_encryption/overrides.rb +1 -0
- data/lib/active_storage_encryption/private_url_policy.rb +4 -2
- data/lib/active_storage_encryption/version.rb +1 -1
- data/lib/active_storage_encryption.rb +1 -0
- data/lib/generators/add_encryption_key_to_active_storage_blobs.rb.erb +9 -0
- data/lib/generators/install_generator.rb +25 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/fixtures/files/.keep +0 -0
- data/test/integration/.keep +0 -0
- data/test/integration/encrypted_blob_proxy_controller_test.rb +253 -0
- data/test/integration/encrypted_blobs_controller_test.rb +0 -130
- data/test/lib/encrypted_disk_service_test.rb +5 -119
- data/test/lib/encrypted_mirror_service_test.rb +1 -1
- data/test/lib/encrypted_s3_service_test.rb +5 -2
- metadata +35 -10
- data/test/dummy/log/test.log +0 -1022
- 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
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
|
6
|
+
module ActiveStorageEncryption
|
7
|
+
# The generator is used to install ActiveStorageEncryption. It adds the `encryption_key`
|
8
|
+
# column to ActiveStorage::Blob.
|
9
|
+
# Run it with `bin/rails g active_storage_encryption:install` in your console.
|
10
|
+
class InstallGenerator < Rails::Generators::Base
|
11
|
+
include ActiveRecord::Generators::Migration
|
12
|
+
|
13
|
+
source_paths << File.join(File.dirname(__FILE__, 2))
|
14
|
+
|
15
|
+
# Generates monolithic migration file that contains all database changes.
|
16
|
+
def create_migration_file
|
17
|
+
# Adding a new migration to the gem is then just adding a file.
|
18
|
+
migration_file_paths_in_order = Dir.glob(__dir__ + "/*.rb.erb").sort
|
19
|
+
migration_file_paths_in_order.each do |migration_template_path|
|
20
|
+
untemplated_migration_filename = File.basename(migration_template_path).gsub(/\.erb$/, "")
|
21
|
+
migration_template(migration_template_path, File.join(db_migrate_path, untemplated_migration_filename))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,253 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class ActiveStorageEncryptionEncryptedBlobProxyControllerTest < ActionDispatch::IntegrationTest
|
6
|
+
setup do
|
7
|
+
@storage_dir = Dir.mktmpdir
|
8
|
+
@other_storage_dir = Dir.mktmpdir
|
9
|
+
@service = ActiveStorageEncryption::EncryptedDiskService.new(root: @storage_dir, private_url_policy: "stream")
|
10
|
+
@service.name = "amazing_encrypting_disk_service" # Needed for the controller and service lookup
|
11
|
+
|
12
|
+
# Hack: sneakily add our service to them configurations
|
13
|
+
# ActiveStorage::Blob.services.send(:services)["amazing_encrypting_disk_service"] = @service
|
14
|
+
|
15
|
+
# We need to set our service as the default, because the controller does lookup from the application config -
|
16
|
+
# which does not include the service we define here
|
17
|
+
@previous_default_service = ActiveStorage::Blob.service
|
18
|
+
@previous_services = ActiveStorage::Blob.services
|
19
|
+
|
20
|
+
# To catch potential issues where something goes to the default service by mistake, let's set a
|
21
|
+
# different Service as the default
|
22
|
+
@non_encrypted_default_service = ActiveStorage::Service::DiskService.new(root: @other_storage_dir)
|
23
|
+
ActiveStorage::Blob.service = @non_encrypted_default_service
|
24
|
+
ActiveStorage::Blob.services = {@service.name => @service} # That too
|
25
|
+
|
26
|
+
# This needs to be set
|
27
|
+
ActiveStorageEncryption::Engine.routes.default_url_options = {host: "www.example.com"}
|
28
|
+
|
29
|
+
# We need to use a hostname for ActiveStorage which is in the Rails authorized hosts.
|
30
|
+
# see https://stackoverflow.com/a/60573259/153886
|
31
|
+
ActiveStorage::Current.url_options = {
|
32
|
+
host: "www.example.com",
|
33
|
+
protocol: "https"
|
34
|
+
}
|
35
|
+
freeze_time # For testing expiring tokens
|
36
|
+
https! # So that all requests are simulated as SSL
|
37
|
+
end
|
38
|
+
|
39
|
+
def teardown
|
40
|
+
unfreeze_time
|
41
|
+
ActiveStorage::Blob.service = @previous_default_service
|
42
|
+
ActiveStorage::Blob.services = @previous_services
|
43
|
+
FileUtils.rm_rf(@storage_dir)
|
44
|
+
FileUtils.rm_rf(@other_storage_dir)
|
45
|
+
end
|
46
|
+
|
47
|
+
def engine_routes
|
48
|
+
ActiveStorageEncryption::Engine.routes.url_helpers
|
49
|
+
end
|
50
|
+
|
51
|
+
test "show() serves the complete decrypted blob body" do
|
52
|
+
rng = Random.new(Minitest.seed)
|
53
|
+
plaintext = rng.bytes(512)
|
54
|
+
|
55
|
+
blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
|
56
|
+
assert blob.encryption_key
|
57
|
+
|
58
|
+
streaming_url = blob.url(disposition: "inline") # This generates a URL with the byte size
|
59
|
+
get streaming_url
|
60
|
+
|
61
|
+
assert_response :success
|
62
|
+
assert_equal "x-office/severance", response.headers["content-type"]
|
63
|
+
assert_equal blob.key.inspect, response.headers["etag"]
|
64
|
+
assert_equal plaintext, response.body
|
65
|
+
end
|
66
|
+
|
67
|
+
test "show() serves a blob of 0 size" do
|
68
|
+
Random.new(Minitest.seed)
|
69
|
+
plaintext = "".b
|
70
|
+
|
71
|
+
blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
|
72
|
+
assert blob.encryption_key
|
73
|
+
|
74
|
+
streaming_url = blob.url(disposition: "inline") # This generates a URL with the byte size
|
75
|
+
get streaming_url
|
76
|
+
|
77
|
+
assert_response :success
|
78
|
+
assert response.body.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
test "show() returns a 404 when the blob no longer exists on the service" do
|
82
|
+
Random.new(Minitest.seed)
|
83
|
+
plaintext = "hello"
|
84
|
+
|
85
|
+
blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
|
86
|
+
assert blob.encryption_key
|
87
|
+
|
88
|
+
streaming_url = blob.url(disposition: "inline") # This generates a URL with the byte size
|
89
|
+
blob.service.delete(blob.key)
|
90
|
+
|
91
|
+
get streaming_url
|
92
|
+
|
93
|
+
assert_response :not_found
|
94
|
+
end
|
95
|
+
|
96
|
+
test "show() serves HTTP ranges" do
|
97
|
+
rng = Random.new(Minitest.seed)
|
98
|
+
plaintext = rng.bytes(5.megabytes + 13)
|
99
|
+
|
100
|
+
blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
|
101
|
+
assert blob.encryption_key
|
102
|
+
|
103
|
+
streaming_url = blob.url(disposition: "inline") # This generates a URL with the byte size
|
104
|
+
get streaming_url, headers: {"Range" => "bytes=0-0"}
|
105
|
+
|
106
|
+
assert_response :partial_content
|
107
|
+
assert_equal "1", response.headers["content-length"]
|
108
|
+
assert_equal "bytes 0-0/5242893", response.headers["content-range"]
|
109
|
+
assert_equal "x-office/severance", response.headers["content-type"]
|
110
|
+
assert_equal plaintext[0..0], response.body
|
111
|
+
|
112
|
+
get streaming_url, headers: {"Range" => "bytes=1-2"}
|
113
|
+
|
114
|
+
assert_response :partial_content
|
115
|
+
assert_equal "2", response.headers["content-length"]
|
116
|
+
assert_equal "bytes 1-2/5242893", response.headers["content-range"]
|
117
|
+
assert_equal "x-office/severance", response.headers["content-type"]
|
118
|
+
assert_equal plaintext[1..2], response.body
|
119
|
+
|
120
|
+
get streaming_url, headers: {"Range" => "bytes=1-2,8-10,12-23"}
|
121
|
+
|
122
|
+
assert_response :partial_content
|
123
|
+
assert response.headers["content-type"].start_with?("multipart/byteranges; boundary=")
|
124
|
+
assert_nil response.headers["content-range"]
|
125
|
+
assert_equal 350, response.body.bytesize
|
126
|
+
|
127
|
+
get streaming_url, headers: {"Range" => "bytes=99999999999999999-99999999999999999"}
|
128
|
+
assert_response :range_not_satisfiable
|
129
|
+
end
|
130
|
+
|
131
|
+
test "show() refuses a request which goes to a non-encrypted Service" do
|
132
|
+
rng = Random.new(Minitest.seed)
|
133
|
+
|
134
|
+
key = SecureRandom.base36(12)
|
135
|
+
encryption_key = rng.bytes(32)
|
136
|
+
plaintext = rng.bytes(512)
|
137
|
+
@service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
|
138
|
+
|
139
|
+
streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"),
|
140
|
+
expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance",
|
141
|
+
blob_byte_size: plaintext.bytesize)
|
142
|
+
|
143
|
+
# Sneak in a non-encrypted service under the same key
|
144
|
+
ActiveStorage::Blob.services[@service.name] = @non_encrypted_default_service
|
145
|
+
|
146
|
+
get streaming_url
|
147
|
+
assert_response :forbidden
|
148
|
+
end
|
149
|
+
|
150
|
+
test "show() refuses a request which has an incorrect encryption key" do
|
151
|
+
rng = Random.new(Minitest.seed)
|
152
|
+
|
153
|
+
key = SecureRandom.base36(12)
|
154
|
+
encryption_key = rng.bytes(32)
|
155
|
+
plaintext = rng.bytes(512)
|
156
|
+
@service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
|
157
|
+
|
158
|
+
another_encryption_key = rng.bytes(32)
|
159
|
+
refute_equal encryption_key, another_encryption_key
|
160
|
+
|
161
|
+
streaming_url = @service.url(key, encryption_key: another_encryption_key,
|
162
|
+
filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds,
|
163
|
+
disposition: "inline", content_type: "x-office/severance", blob_byte_size: plaintext.bytesize)
|
164
|
+
get streaming_url
|
165
|
+
|
166
|
+
assert_response :forbidden
|
167
|
+
end
|
168
|
+
|
169
|
+
test "show() refuses a request with a garbage token" do
|
170
|
+
get engine_routes.encrypted_blob_streaming_get_path(token: "garbage", filename: "exfil.bin")
|
171
|
+
assert_response :forbidden
|
172
|
+
end
|
173
|
+
|
174
|
+
test "show() refuses a request with a token that has been encrypted using an incorrect encryption key" do
|
175
|
+
https!
|
176
|
+
rng = Random.new(Minitest.seed)
|
177
|
+
encryptor_key = rng.bytes(32)
|
178
|
+
other_encryptor = ActiveStorageEncryption::TokenEncryptor.new(encryptor_key, url_safe: encryptor_key)
|
179
|
+
|
180
|
+
key = SecureRandom.base36(12)
|
181
|
+
encryption_key = rng.bytes(32)
|
182
|
+
@service.upload(key, StringIO.new(rng.bytes(512)).binmode, encryption_key: encryption_key)
|
183
|
+
|
184
|
+
streaming_url = ActiveStorageEncryption.stub(:token_encryptor, -> { other_encryptor }) do
|
185
|
+
@service.url(key, encryption_key: encryption_key,
|
186
|
+
filename: ActiveStorage::Filename.new("private.doc"), expires_in: 3.seconds,
|
187
|
+
disposition: "inline", content_type: "binary/octet-stream",
|
188
|
+
blob_byte_size: 512)
|
189
|
+
end
|
190
|
+
|
191
|
+
get streaming_url
|
192
|
+
assert_response :forbidden
|
193
|
+
end
|
194
|
+
|
195
|
+
test "show() refuses a request with a token that has expired" do
|
196
|
+
rng = Random.new(Minitest.seed)
|
197
|
+
|
198
|
+
key = SecureRandom.base36(12)
|
199
|
+
encryption_key = rng.bytes(32)
|
200
|
+
@service.upload(key, StringIO.new(rng.bytes(512)).binmode, encryption_key: encryption_key)
|
201
|
+
|
202
|
+
streaming_url = @service.url(key, encryption_key: encryption_key,
|
203
|
+
filename: ActiveStorage::Filename.new("private.doc"), expires_in: 3.seconds,
|
204
|
+
disposition: "inline", content_type: "binary/octet-stream",
|
205
|
+
blob_byte_size: 512)
|
206
|
+
travel 5.seconds
|
207
|
+
|
208
|
+
get streaming_url
|
209
|
+
assert_response :forbidden
|
210
|
+
end
|
211
|
+
|
212
|
+
test "show() requires headers if the private_url_policy of the service is set to :require_headers" do
|
213
|
+
rng = Random.new(Minitest.seed)
|
214
|
+
|
215
|
+
key = SecureRandom.base36(12)
|
216
|
+
encryption_key = rng.bytes(32)
|
217
|
+
plaintext = rng.bytes(512)
|
218
|
+
@service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
|
219
|
+
|
220
|
+
# The policy needs to be set before we generate the token (the token includes require_headers)
|
221
|
+
@service.private_url_policy = :require_headers
|
222
|
+
streaming_url = @service.url(key, encryption_key: encryption_key,
|
223
|
+
filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline",
|
224
|
+
content_type: "x-office/severance", blob_byte_size: plaintext.bytesize)
|
225
|
+
|
226
|
+
get streaming_url
|
227
|
+
assert_response :forbidden # Without headers
|
228
|
+
|
229
|
+
get streaming_url, headers: {"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(encryption_key)}
|
230
|
+
assert_response :success
|
231
|
+
assert_equal "x-office/severance", response.headers["content-type"]
|
232
|
+
assert_equal plaintext, response.body
|
233
|
+
end
|
234
|
+
|
235
|
+
test "show() refuses a request if the service no longer permits private URLs, even if the URL was generated when it used to permit them" do
|
236
|
+
rng = Random.new(Minitest.seed)
|
237
|
+
|
238
|
+
SecureRandom.base36(12)
|
239
|
+
plaintext = rng.bytes(512)
|
240
|
+
|
241
|
+
blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(plaintext), content_type: "x-office/severance", filename: "secret.bin", service_name: @service.name)
|
242
|
+
assert blob.encryption_key
|
243
|
+
streaming_url = blob.url(disposition: "inline", content_type: "x-office/severance")
|
244
|
+
|
245
|
+
@service.private_url_policy = :disable
|
246
|
+
|
247
|
+
get streaming_url
|
248
|
+
assert_response :forbidden # Without headers
|
249
|
+
|
250
|
+
get streaming_url, headers: {"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(blob.encryption_key)}
|
251
|
+
assert_response :forbidden # With headers
|
252
|
+
end
|
253
|
+
end
|
@@ -9,9 +9,6 @@ class ActiveStorageEncryptionEncryptedBlobsControllerTest < ActionDispatch::Inte
|
|
9
9
|
@service = ActiveStorageEncryption::EncryptedDiskService.new(root: @storage_dir, private_url_policy: "stream")
|
10
10
|
@service.name = "amazing_encrypting_disk_service" # Needed for the controller and service lookup
|
11
11
|
|
12
|
-
# Hack: sneakily add our service to them configurations
|
13
|
-
# ActiveStorage::Blob.services.send(:services)["amazing_encrypting_disk_service"] = @service
|
14
|
-
|
15
12
|
# We need to set our service as the default, because the controller does lookup from the application config -
|
16
13
|
# which does not include the service we define here
|
17
14
|
@previous_default_service = ActiveStorage::Blob.service
|
@@ -48,133 +45,6 @@ class ActiveStorageEncryptionEncryptedBlobsControllerTest < ActionDispatch::Inte
|
|
48
45
|
ActiveStorageEncryption::Engine.routes.url_helpers
|
49
46
|
end
|
50
47
|
|
51
|
-
test "show() returns the decrypted blob body" do
|
52
|
-
rng = Random.new(Minitest.seed)
|
53
|
-
|
54
|
-
key = SecureRandom.base36(12)
|
55
|
-
encryption_key = rng.bytes(32)
|
56
|
-
plaintext = rng.bytes(512)
|
57
|
-
@service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
|
58
|
-
|
59
|
-
streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
|
60
|
-
get streaming_url
|
61
|
-
|
62
|
-
assert_response :success
|
63
|
-
assert_equal "x-office/severance", response.headers["content-type"]
|
64
|
-
assert_equal plaintext, response.body
|
65
|
-
end
|
66
|
-
|
67
|
-
test "show() refuses a request which goes to a non-encrypted Service" do
|
68
|
-
rng = Random.new(Minitest.seed)
|
69
|
-
|
70
|
-
key = SecureRandom.base36(12)
|
71
|
-
encryption_key = rng.bytes(32)
|
72
|
-
plaintext = rng.bytes(512)
|
73
|
-
@service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
|
74
|
-
|
75
|
-
streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
|
76
|
-
|
77
|
-
# Sneak in a non-encrypted service under the same key
|
78
|
-
ActiveStorage::Blob.services[@service.name] = @non_encrypted_default_service
|
79
|
-
|
80
|
-
get streaming_url
|
81
|
-
assert_response :forbidden
|
82
|
-
end
|
83
|
-
|
84
|
-
test "show() refuses a request which has an incorrect encryption key" do
|
85
|
-
rng = Random.new(Minitest.seed)
|
86
|
-
|
87
|
-
key = SecureRandom.base36(12)
|
88
|
-
encryption_key = rng.bytes(32)
|
89
|
-
plaintext = rng.bytes(512)
|
90
|
-
@service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
|
91
|
-
|
92
|
-
another_encryption_key = rng.bytes(32)
|
93
|
-
refute_equal encryption_key, another_encryption_key
|
94
|
-
|
95
|
-
streaming_url = @service.url(key, encryption_key: another_encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
|
96
|
-
get streaming_url
|
97
|
-
|
98
|
-
assert_response :forbidden
|
99
|
-
end
|
100
|
-
|
101
|
-
test "show() refuses a request with a garbage token" do
|
102
|
-
get engine_routes.encrypted_blob_streaming_get_path(token: "garbage", filename: "exfil.bin")
|
103
|
-
assert_response :forbidden
|
104
|
-
end
|
105
|
-
|
106
|
-
test "show() refuses a request with a token that has been encrypted using an incorrect encryption key" do
|
107
|
-
https!
|
108
|
-
rng = Random.new(Minitest.seed)
|
109
|
-
encryptor_key = rng.bytes(32)
|
110
|
-
other_encryptor = ActiveStorageEncryption::TokenEncryptor.new(encryptor_key, url_safe: encryptor_key)
|
111
|
-
|
112
|
-
key = SecureRandom.base36(12)
|
113
|
-
encryption_key = rng.bytes(32)
|
114
|
-
@service.upload(key, StringIO.new(rng.bytes(512)).binmode, encryption_key: encryption_key)
|
115
|
-
|
116
|
-
streaming_url = ActiveStorageEncryption.stub(:token_encryptor, -> { other_encryptor }) do
|
117
|
-
@service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 3.seconds, disposition: "inline", content_type: "binary/octet-stream")
|
118
|
-
end
|
119
|
-
|
120
|
-
get streaming_url
|
121
|
-
assert_response :forbidden
|
122
|
-
end
|
123
|
-
|
124
|
-
test "show() refuses a request with a token that has expired" do
|
125
|
-
rng = Random.new(Minitest.seed)
|
126
|
-
|
127
|
-
key = SecureRandom.base36(12)
|
128
|
-
encryption_key = rng.bytes(32)
|
129
|
-
@service.upload(key, StringIO.new(rng.bytes(512)).binmode, encryption_key: encryption_key)
|
130
|
-
|
131
|
-
streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 3.seconds, disposition: "inline", content_type: "binary/octet-stream")
|
132
|
-
travel 5.seconds
|
133
|
-
|
134
|
-
get streaming_url
|
135
|
-
assert_response :forbidden
|
136
|
-
end
|
137
|
-
|
138
|
-
test "show() requires headers if the private_url_policy of the service is set to :require_headers" do
|
139
|
-
rng = Random.new(Minitest.seed)
|
140
|
-
|
141
|
-
key = SecureRandom.base36(12)
|
142
|
-
encryption_key = rng.bytes(32)
|
143
|
-
plaintext = rng.bytes(512)
|
144
|
-
@service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
|
145
|
-
|
146
|
-
# The policy needs to be set before we generate the token (the token includes require_headers)
|
147
|
-
@service.private_url_policy = :require_headers
|
148
|
-
streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
|
149
|
-
|
150
|
-
get streaming_url
|
151
|
-
assert_response :forbidden # Without headers
|
152
|
-
|
153
|
-
get streaming_url, headers: {"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(encryption_key)}
|
154
|
-
assert_response :success
|
155
|
-
assert_equal "x-office/severance", response.headers["content-type"]
|
156
|
-
assert_equal plaintext, response.body
|
157
|
-
end
|
158
|
-
|
159
|
-
test "show() refuses a request if the service no longer permits private URLs" do
|
160
|
-
rng = Random.new(Minitest.seed)
|
161
|
-
|
162
|
-
key = SecureRandom.base36(12)
|
163
|
-
encryption_key = rng.bytes(32)
|
164
|
-
plaintext = rng.bytes(512)
|
165
|
-
@service.upload(key, StringIO.new(plaintext).binmode, encryption_key: encryption_key)
|
166
|
-
|
167
|
-
streaming_url = @service.url(key, encryption_key: encryption_key, filename: ActiveStorage::Filename.new("private.doc"), expires_in: 30.seconds, disposition: "inline", content_type: "x-office/severance")
|
168
|
-
|
169
|
-
@service.private_url_policy = :disable
|
170
|
-
|
171
|
-
get streaming_url
|
172
|
-
assert_response :forbidden # Without headers
|
173
|
-
|
174
|
-
get streaming_url, headers: {"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(encryption_key)}
|
175
|
-
assert_response :forbidden # Without headers
|
176
|
-
end
|
177
|
-
|
178
48
|
test "create_direct_upload creates a blob and returns the headers and the URL to start the upload, which are for the correct service name" do
|
179
49
|
rng = Random.new(Minitest.seed)
|
180
50
|
plaintext = rng.bytes(512)
|
@@ -91,95 +91,15 @@ class ActiveStorageEncryption::EncryptedDiskServiceTest < ActiveSupport::TestCas
|
|
91
91
|
assert_equal Digest::SHA256.hexdigest(plaintext_upload_bytes), Digest::SHA256.hexdigest(readback_bytes)
|
92
92
|
end
|
93
93
|
|
94
|
-
def
|
95
|
-
@service.private_url_policy = :require_headers
|
96
|
-
|
97
|
-
key = "key-1"
|
98
|
-
k = Random.bytes(68)
|
99
|
-
plaintext_upload_bytes = generate_random_binary_string
|
100
|
-
@service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key: k)
|
101
|
-
|
102
|
-
ActiveStorage::Blob.service = @service # So that the controller can find it
|
103
|
-
|
104
|
-
# ActiveStorage wraps the passed filename in a wrapper thingy
|
105
|
-
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
106
|
-
url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
|
107
|
-
assert url.include?("/active-storage-encryption/blob/")
|
108
|
-
|
109
|
-
uri = URI.parse(url)
|
110
|
-
|
111
|
-
# Do a super-minimalistic test on the DiskController. ActionController is actually a Rack app (or, rather: every controller action is a Rack app).
|
112
|
-
# It can thus be called with a minimal Rack env. For the definition of "minimal", see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-environment-
|
113
|
-
rack_env = {
|
114
|
-
"SCRIPT_NAME" => "",
|
115
|
-
"PATH_INFO" => uri.path,
|
116
|
-
"QUERY_STRING" => uri.query,
|
117
|
-
"REQUEST_METHOD" => "GET",
|
118
|
-
"SERVER_NAME" => uri.host,
|
119
|
-
"HTTP_X_ACTIVE_STORAGE_ENCRYPTION_KEY" => Base64.strict_encode64(k),
|
120
|
-
"rack.input" => StringIO.new(""),
|
121
|
-
"action_dispatch.request.parameters" => {
|
122
|
-
# The controller expects the Rails router to have injected this param by extracting
|
123
|
-
# it from the route path. The upload param is mapped to :encoded_token, the download param is
|
124
|
-
# mapped to :encoded_key - likely because there was an exploit with ActiveStorage where keys
|
125
|
-
# generated for download could be used for uploading (and thus - overwriting)
|
126
|
-
"token" => uri.path.split("/")[-2] # For "show", the last path param is actually the filename - this is because Content-Disposition can be unreliable for download filename
|
127
|
-
}
|
128
|
-
}
|
129
|
-
action_app = ActiveStorageEncryption::EncryptedBlobsController.action(:show)
|
130
|
-
status, _headers, body = action_app.call(rack_env)
|
131
|
-
|
132
|
-
assert_equal 200, status
|
133
|
-
|
134
|
-
readback_bytes = (+"").b.tap do |buf|
|
135
|
-
body.each { |chunk| buf << chunk }
|
136
|
-
end
|
137
|
-
assert_equal Digest::SHA256.hexdigest(plaintext_upload_bytes), Digest::SHA256.hexdigest(readback_bytes)
|
138
|
-
end
|
139
|
-
|
140
|
-
def test_get_without_headers_succeeds_if_service_permits
|
94
|
+
def test_private_url
|
141
95
|
@service.private_url_policy = :stream
|
142
96
|
|
143
|
-
key = "key-1"
|
144
|
-
k = Random.bytes(68)
|
145
|
-
plaintext_upload_bytes = generate_random_binary_string
|
146
|
-
@service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key: k)
|
147
|
-
|
148
|
-
ActiveStorage::Blob.service = @service # So that the controller can find it
|
149
|
-
|
150
97
|
# ActiveStorage wraps the passed filename in a wrapper thingy
|
151
98
|
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
152
|
-
|
99
|
+
key = "key-1"
|
100
|
+
encryption_key = Random.bytes(32)
|
101
|
+
url = @service.url(key, blob_byte_size: 14, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key:, expires_in: 10.seconds)
|
153
102
|
assert url.include?("/active-storage-encryption/blob/")
|
154
|
-
|
155
|
-
uri = URI.parse(url)
|
156
|
-
|
157
|
-
# Do a super-minimalistic test on the DiskController. ActionController is actually a Rack app (or, rather: every controller action is a Rack app).
|
158
|
-
# It can thus be called with a minimal Rack env. For the definition of "minimal", see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-environment-
|
159
|
-
rack_env = {
|
160
|
-
"SCRIPT_NAME" => "",
|
161
|
-
"PATH_INFO" => uri.path,
|
162
|
-
"QUERY_STRING" => uri.query,
|
163
|
-
"REQUEST_METHOD" => "GET",
|
164
|
-
"SERVER_NAME" => uri.host,
|
165
|
-
"rack.input" => StringIO.new(""),
|
166
|
-
"action_dispatch.request.parameters" => {
|
167
|
-
# The controller expects the Rails router to have injected this param by extracting
|
168
|
-
# it from the route path. The upload param is mapped to :encoded_token, the download param is
|
169
|
-
# mapped to :encoded_key - likely because there was an exploit with ActiveStorage where keys
|
170
|
-
# generated for download could be used for uploading (and thus - overwriting)
|
171
|
-
"token" => uri.path.split("/")[-2] # For "show", the last path param is actually the filename - this is because Content-Disposition can be unreliable for download filename
|
172
|
-
}
|
173
|
-
}
|
174
|
-
action_app = ActiveStorageEncryption::EncryptedBlobsController.action(:show)
|
175
|
-
status, _headers, body = action_app.call(rack_env)
|
176
|
-
|
177
|
-
assert_equal 200, status
|
178
|
-
|
179
|
-
readback_bytes = (+"").b.tap do |buf|
|
180
|
-
body.each { |chunk| buf << chunk }
|
181
|
-
end
|
182
|
-
assert_equal Digest::SHA256.hexdigest(plaintext_upload_bytes), Digest::SHA256.hexdigest(readback_bytes)
|
183
103
|
end
|
184
104
|
|
185
105
|
def test_generating_url_fails_if_streaming_is_off_for_the_service
|
@@ -193,44 +113,10 @@ class ActiveStorageEncryption::EncryptedDiskServiceTest < ActiveSupport::TestCas
|
|
193
113
|
# ActiveStorage wraps the passed filename in a wrapper thingy
|
194
114
|
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
195
115
|
assert_raises ActiveStorageEncryption::StreamingDisabled do
|
196
|
-
@service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
|
116
|
+
@service.url(key, blob_byte_size: 12, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
|
197
117
|
end
|
198
118
|
end
|
199
119
|
|
200
|
-
def test_get_without_headers_fails_if_service_does_not_permit
|
201
|
-
@service.private_url_policy = :require_headers
|
202
|
-
|
203
|
-
key = "key-1"
|
204
|
-
k = Random.bytes(68)
|
205
|
-
plaintext_upload_bytes = generate_random_binary_string
|
206
|
-
@service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key: k)
|
207
|
-
|
208
|
-
ActiveStorage::Blob.service = @service # So that the controller can find it
|
209
|
-
|
210
|
-
# ActiveStorage wraps the passed filename in a wrapper thingy
|
211
|
-
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
212
|
-
url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
|
213
|
-
uri = URI.parse(url)
|
214
|
-
|
215
|
-
# Do a super-minimalistic test on the DiskController. ActionController is actually a Rack app (or, rather: every controller action is a Rack app).
|
216
|
-
# It can thus be called with a minimal Rack env. For the definition of "minimal", see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-environment-
|
217
|
-
rack_env = {
|
218
|
-
"SCRIPT_NAME" => "",
|
219
|
-
"PATH_INFO" => uri.path,
|
220
|
-
"QUERY_STRING" => uri.query,
|
221
|
-
"REQUEST_METHOD" => "GET",
|
222
|
-
"SERVER_NAME" => uri.host,
|
223
|
-
"rack.input" => StringIO.new(""),
|
224
|
-
# Omit x-disk-encryption-key
|
225
|
-
"action_dispatch.request.parameters" => {
|
226
|
-
"token" => uri.path.split("/")[-2] # For "show", the last path param is actually the filename - this is because Content-Disposition can be unreliable for download filename
|
227
|
-
}
|
228
|
-
}
|
229
|
-
action_app = ActiveStorageEncryption::EncryptedBlobsController.action(:show)
|
230
|
-
status, _headers, _body = action_app.call(rack_env)
|
231
|
-
assert_equal 403, status
|
232
|
-
end
|
233
|
-
|
234
120
|
def test_upload_then_download_using_correct_key
|
235
121
|
storage_blob_key = "key-1"
|
236
122
|
k = Random.bytes(68)
|
@@ -119,7 +119,7 @@ class ActiveStorageEncryption::EncryptedMirrorServiceTest < ActiveSupport::TestC
|
|
119
119
|
|
120
120
|
# ActiveStorage wraps the passed filename in a wrapper thingy
|
121
121
|
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
122
|
-
url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
|
122
|
+
url = @service.url(key, blob_byte_size: 13, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
|
123
123
|
assert url.include?("/active-storage-encryption/blob/")
|
124
124
|
end
|
125
125
|
|
@@ -73,7 +73,9 @@ class ActiveStorageEncryption::EncryptedS3ServiceTest < ActiveSupport::TestCase
|
|
73
73
|
|
74
74
|
# ActiveStorage wraps the passed filename in a wrapper thingy
|
75
75
|
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
76
|
-
url = @service.url(key,
|
76
|
+
url = @service.url(key, blob_byte_size: plaintext_upload_bytes.bytesize,
|
77
|
+
filename: filename_with_sanitization, content_type: "binary/octet-stream",
|
78
|
+
disposition: "inline", encryption_key: k, expires_in: 10.seconds)
|
77
79
|
assert url.include?("/active-storage-encryption/blob/")
|
78
80
|
end
|
79
81
|
|
@@ -88,7 +90,8 @@ class ActiveStorageEncryption::EncryptedS3ServiceTest < ActiveSupport::TestCase
|
|
88
90
|
|
89
91
|
# ActiveStorage wraps the passed filename in a wrapper thingy
|
90
92
|
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
91
|
-
url = @service.url(key,
|
93
|
+
url = @service.url(key, blob_byte_size: plaintext_upload_bytes.bytesize,
|
94
|
+
filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 240.seconds)
|
92
95
|
|
93
96
|
assert url.include?("x-amz-server-side-encryption-customer-algorithm")
|
94
97
|
refute url.include?("x-amz-server-side-encryption-customer-key=") # The key should not be in the URL
|