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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/workflows/ci.yml +75 -0
  4. data/.gitignore +14 -0
  5. data/.ruby-version +1 -0
  6. data/.standard.yml +1 -0
  7. data/Appraisals +2 -0
  8. data/Gemfile +9 -0
  9. data/README.md +24 -3
  10. data/Rakefile +0 -2
  11. data/active_storage_encryption.gemspec +45 -0
  12. data/config/routes.rb +1 -1
  13. data/gemfiles/rails_7.gemfile +1 -0
  14. data/gemfiles/rails_7.gemfile.lock +10 -1
  15. data/gemfiles/rails_8.gemfile +1 -0
  16. data/gemfiles/rails_8.gemfile.lock +10 -1
  17. data/lib/active_storage_encryption/encrypted_blob_proxy_controller.rb +116 -0
  18. data/lib/active_storage_encryption/encrypted_blobs_controller.rb +0 -51
  19. data/lib/active_storage_encryption/encrypted_s3_service.rb +1 -0
  20. data/lib/active_storage_encryption/engine.rb +4 -0
  21. data/lib/active_storage_encryption/overrides.rb +1 -0
  22. data/lib/active_storage_encryption/private_url_policy.rb +4 -2
  23. data/lib/active_storage_encryption/version.rb +1 -1
  24. data/lib/active_storage_encryption.rb +1 -0
  25. data/lib/generators/add_encryption_key_to_active_storage_blobs.rb.erb +9 -0
  26. data/lib/generators/install_generator.rb +25 -0
  27. data/test/dummy/app/assets/images/.keep +0 -0
  28. data/test/dummy/app/controllers/concerns/.keep +0 -0
  29. data/test/dummy/app/models/concerns/.keep +0 -0
  30. data/test/dummy/lib/assets/.keep +0 -0
  31. data/test/dummy/log/.keep +0 -0
  32. data/test/fixtures/files/.keep +0 -0
  33. data/test/integration/.keep +0 -0
  34. data/test/integration/encrypted_blob_proxy_controller_test.rb +253 -0
  35. data/test/integration/encrypted_blobs_controller_test.rb +0 -130
  36. data/test/lib/encrypted_disk_service_test.rb +5 -119
  37. data/test/lib/encrypted_mirror_service_test.rb +1 -1
  38. data/test/lib/encrypted_s3_service_test.rb +5 -2
  39. metadata +35 -10
  40. data/test/dummy/log/test.log +0 -1022
  41. data/test/dummy/storage/test.sqlite3 +0 -0
  42. data/test/dummy/storage/x6/pl/x6plznfuhrsyjn9pox2a6xgmcs3x +0 -0
  43. 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 test_get_with_headers_always_succeeds
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
- url = @service.url(key, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
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, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 10.seconds)
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, filename: filename_with_sanitization, content_type: "binary/octet-stream", disposition: "inline", encryption_key: k, expires_in: 240.seconds)
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