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
@@ -10,6 +10,7 @@ module ActiveStorageEncryption
|
|
10
10
|
autoload :EncryptedDiskService, __dir__ + "/active_storage_encryption/encrypted_disk_service.rb"
|
11
11
|
autoload :EncryptedMirrorService, __dir__ + "/active_storage_encryption/encrypted_mirror_service.rb"
|
12
12
|
autoload :EncryptedS3Service, __dir__ + "/active_storage_encryption/encrypted_s3_service.rb"
|
13
|
+
autoload :EncryptedGCSService, __dir__ + "/active_storage_encryption/encrypted_gcs_service.rb"
|
13
14
|
autoload :Overrides, __dir__ + "/active_storage_encryption/overrides.rb"
|
14
15
|
|
15
16
|
class IncorrectEncryptionKey < ArgumentError
|
@@ -42,6 +42,8 @@ Rails.application.configure do
|
|
42
42
|
# Tell Active Support which deprecation messages to disallow.
|
43
43
|
config.active_support.disallowed_deprecation_warnings = []
|
44
44
|
|
45
|
+
config.active_storage.service = :test
|
46
|
+
|
45
47
|
# Raises error for missing translations.
|
46
48
|
# config.i18n.raise_on_missing_translations = true
|
47
49
|
|
data/test/dummy/db/schema.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
# This file is auto-generated from the current state of the database. Instead
|
4
2
|
# of editing this file, please use the migrations feature of Active Record to
|
5
3
|
# incrementally modify your database, and then regenerate this schema definition.
|
@@ -12,7 +10,7 @@
|
|
12
10
|
#
|
13
11
|
# It's strongly recommended that you check this file into your version control system.
|
14
12
|
|
15
|
-
ActiveRecord::Schema[7.2].define(version:
|
13
|
+
ActiveRecord::Schema[7.2].define(version: 2025_04_28_093315) do
|
16
14
|
create_table "active_storage_attachments", force: :cascade do |t|
|
17
15
|
t.string "name", null: false
|
18
16
|
t.string "record_type", null: false
|
@@ -42,6 +40,11 @@ ActiveRecord::Schema[7.2].define(version: 2025_03_04_023853) do
|
|
42
40
|
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
43
41
|
end
|
44
42
|
|
43
|
+
create_table "users", force: :cascade do |t|
|
44
|
+
t.datetime "created_at", null: false
|
45
|
+
t.datetime "updated_at", null: false
|
46
|
+
end
|
47
|
+
|
45
48
|
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
46
49
|
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
47
50
|
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class ActiveStorageEncryption::EncryptedGCSServiceTest < ActiveSupport::TestCase
|
6
|
+
def config
|
7
|
+
{
|
8
|
+
project_id: "sandbox-ci-25b8",
|
9
|
+
bucket: "sandbox-ci-testing-secure-documents",
|
10
|
+
private_url_policy: "stream"
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
setup do
|
15
|
+
if ENV["GOOGLE_APPLICATION_CREDENTIALS"].blank?
|
16
|
+
skip "You need GOOGLE_APPLICATION_CREDENTIALS set in your env and it needs to point to the JSON keyfile for GCS"
|
17
|
+
end
|
18
|
+
|
19
|
+
@textfile = StringIO.new("Secure document that needs to be stored encrypted.")
|
20
|
+
@textfile2 = StringIO.new("While being neatly organized all in a days work aat the job.")
|
21
|
+
@service = ActiveStorageEncryption::EncryptedGCSService.new(**config)
|
22
|
+
@service.name = "encrypted_gcs_service"
|
23
|
+
|
24
|
+
@encryption_key = ActiveStorage::Blob.generate_random_encryption_key
|
25
|
+
@gcs_key_length_range = (0...ActiveStorageEncryption::EncryptedGCSService::GCS_ENCRYPTION_KEY_LENGTH_BYTES) # 32 bytes
|
26
|
+
end
|
27
|
+
|
28
|
+
def run_id
|
29
|
+
# We use a shared GCS bucket, and multiple runs of the test suite may write into it at the same time.
|
30
|
+
# To prevent clobbering and conflicts, assign a "test run ID" and mix it into the object keys. Keep that
|
31
|
+
# value stable across the test suite.
|
32
|
+
@test_suite_run_id ||= SecureRandom.base36(10)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_encrypted_question_method
|
36
|
+
assert @service.encrypted?
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_forbids_private_urls_with_disabled_policy
|
40
|
+
@service.private_url_policy = :disable
|
41
|
+
|
42
|
+
rng = Random.new(Minitest.seed)
|
43
|
+
key = "#{run_id}-streamed-key-#{rng.hex(4)}"
|
44
|
+
encryption_key = Random.bytes(68)
|
45
|
+
plaintext_upload_bytes = rng.bytes(425)
|
46
|
+
@service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key:)
|
47
|
+
|
48
|
+
# ActiveStorage wraps the passed filename in a wrapper thingy
|
49
|
+
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
50
|
+
|
51
|
+
assert_raises(ActiveStorageEncryption::StreamingDisabled) do
|
52
|
+
@service.url(key, filename: filename_with_sanitization, blob_byte_size: plaintext_upload_bytes.bytesize, content_type: "binary/octet-stream", disposition: "inline", encryption_key:, expires_in: 10.seconds)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_exists
|
57
|
+
rng = Random.new(Minitest.seed)
|
58
|
+
|
59
|
+
key = "#{run_id}-encrypted-exists-key-#{rng.hex(4)}"
|
60
|
+
encryption_key = rng.bytes(47) # Make it bigger than required, to ensure the service truncates it
|
61
|
+
plaintext_upload_bytes = rng.bytes(1024)
|
62
|
+
|
63
|
+
assert_nothing_raised { @service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key:) }
|
64
|
+
refute @service.exist?(key + "-definitely-not-present")
|
65
|
+
assert @service.exist?(key)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_generates_private_streaming_urls_with_streaming_policy
|
69
|
+
@service.private_url_policy = :stream
|
70
|
+
|
71
|
+
rng = Random.new(Minitest.seed)
|
72
|
+
key = "#{run_id}-streamed-key-#{rng.hex(4)}"
|
73
|
+
encryption_key = Random.bytes(68)
|
74
|
+
plaintext_upload_bytes = rng.bytes(425)
|
75
|
+
@service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key:)
|
76
|
+
|
77
|
+
# The streaming URL generation uses Rails routing, so it needs
|
78
|
+
# ActiveStorage::Current.url_options to be set
|
79
|
+
# We need to use a hostname for ActiveStorage which is in the Rails authorized hosts.
|
80
|
+
# see https://stackoverflow.com/a/60573259/153886
|
81
|
+
ActiveStorage::Current.url_options = {
|
82
|
+
host: "www.example.com",
|
83
|
+
protocol: "https"
|
84
|
+
}
|
85
|
+
|
86
|
+
# ActiveStorage wraps the passed filename in a wrapper thingy
|
87
|
+
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
88
|
+
url = @service.url(key, blob_byte_size: plaintext_upload_bytes.bytesize,
|
89
|
+
filename: filename_with_sanitization, content_type: "binary/octet-stream",
|
90
|
+
disposition: "inline", encryption_key:, expires_in: 10.seconds)
|
91
|
+
assert url.include?("/active-storage-encryption/blob/")
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_generates_private_urls_with_require_headers_policy
|
95
|
+
@service.private_url_policy = :require_headers
|
96
|
+
|
97
|
+
rng = Random.new(Minitest.seed)
|
98
|
+
key = "#{run_id}-streamed-key-#{rng.hex(4)}"
|
99
|
+
encryption_key = Random.bytes(68)
|
100
|
+
plaintext_upload_bytes = rng.bytes(425)
|
101
|
+
@service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key:)
|
102
|
+
|
103
|
+
# ActiveStorage wraps the passed filename in a wrapper thingy
|
104
|
+
filename_with_sanitization = ActiveStorage::Filename.new("temp.bin")
|
105
|
+
url = @service.url(key, blob_byte_size: plaintext_upload_bytes.bytesize,
|
106
|
+
filename: filename_with_sanitization, content_type: "binary/octet-stream",
|
107
|
+
disposition: "inline", encryption_key:, expires_in: 240.seconds)
|
108
|
+
|
109
|
+
query_params_hash = URI.decode_www_form(URI.parse(url).query).to_h
|
110
|
+
|
111
|
+
# Downcased header names for this test since that's what we get back from signing process.
|
112
|
+
expected_headers = ["x-goog-encryption-algorithm", "x-goog-encryption-key", "x-goog-encryption-key-sha256"]
|
113
|
+
signed_headers = query_params_hash["X-Goog-SignedHeaders"].split(";")
|
114
|
+
assert expected_headers.all? { |header| header.in?(signed_headers) }
|
115
|
+
|
116
|
+
uri = URI(url)
|
117
|
+
req = Net::HTTP::Get.new(uri)
|
118
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http|
|
119
|
+
http.request(req)
|
120
|
+
}
|
121
|
+
assert_equal "400", res.code
|
122
|
+
|
123
|
+
# TODO make this a headers_for_private_download like in the s3 service
|
124
|
+
download_headers = {
|
125
|
+
"content-type" => "binary/octet-stream",
|
126
|
+
"Content-Disposition" => "inline; filename=\"temp.bin\"; filename*=UTF-8''temp.bin",
|
127
|
+
"x-goog-encryption-algorithm" => "AES256",
|
128
|
+
"x-goog-encryption-key" => Base64.strict_encode64(encryption_key[@gcs_key_length_range]),
|
129
|
+
"x-goog-encryption-key-sha256" => Digest::SHA256.base64digest(encryption_key[@gcs_key_length_range])
|
130
|
+
}
|
131
|
+
download_headers.each_pair { |key, value| req[key] = value }
|
132
|
+
|
133
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http|
|
134
|
+
http.request(req)
|
135
|
+
}
|
136
|
+
assert_equal "200", res.code
|
137
|
+
assert_equal plaintext_upload_bytes, res.body
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_basic_gcs_readback
|
141
|
+
rng = Random.new(Minitest.seed)
|
142
|
+
|
143
|
+
key = "#{run_id}-encrypted-key-#{rng.hex(4)}"
|
144
|
+
encryption_key = rng.bytes(47) # Make it bigger than required, to ensure the service truncates it
|
145
|
+
plaintext_upload_bytes = rng.bytes(1024)
|
146
|
+
|
147
|
+
assert_nothing_raised do
|
148
|
+
@service.upload(key, StringIO.new(plaintext_upload_bytes), encryption_key:)
|
149
|
+
end
|
150
|
+
readback = @service.download(key, encryption_key:)
|
151
|
+
assert_equal readback, plaintext_upload_bytes
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_accepts_direct_upload_with_signature_and_headers
|
155
|
+
rng = Random.new(Minitest.seed)
|
156
|
+
|
157
|
+
key = "#{run_id}-encrypted-key-direct-upload-#{rng.hex(4)}"
|
158
|
+
encryption_key = rng.bytes(47) # Make it bigger than required, to ensure the service truncates it
|
159
|
+
plaintext_upload_bytes = rng.bytes(1024)
|
160
|
+
|
161
|
+
url = @service.url_for_direct_upload(key,
|
162
|
+
encryption_key:,
|
163
|
+
expires_in: 5.minutes.to_i,
|
164
|
+
content_type: "binary/octet-stream",
|
165
|
+
content_length: plaintext_upload_bytes.bytesize,
|
166
|
+
checksum: Digest::MD5.base64digest(plaintext_upload_bytes))
|
167
|
+
|
168
|
+
query_params_hash = URI.decode_www_form(URI.parse(url).query).to_h
|
169
|
+
|
170
|
+
# Downcased header names for this test since that's what we get back from signing process.
|
171
|
+
expected_headers = ["content-md5", "x-goog-encryption-algorithm", "x-goog-encryption-key", "x-goog-encryption-key-sha256"]
|
172
|
+
signed_headers = query_params_hash["X-Goog-SignedHeaders"].split(";")
|
173
|
+
assert expected_headers.all? { |header| header.in?(signed_headers) }
|
174
|
+
|
175
|
+
assert_equal "300", query_params_hash["X-Goog-Expires"]
|
176
|
+
|
177
|
+
should_be_headers = {
|
178
|
+
"Content-Type" => "binary/octet-stream",
|
179
|
+
"Content-MD5" => Digest::MD5.base64digest(plaintext_upload_bytes),
|
180
|
+
"x-goog-encryption-algorithm" => "AES256",
|
181
|
+
"x-goog-encryption-key" => Base64.strict_encode64(encryption_key[@gcs_key_length_range]),
|
182
|
+
"x-goog-encryption-key-sha256" => Digest::SHA256.base64digest(encryption_key[@gcs_key_length_range])
|
183
|
+
}
|
184
|
+
|
185
|
+
headers = @service.headers_for_direct_upload(key,
|
186
|
+
encryption_key:,
|
187
|
+
content_type: "binary/octet-stream",
|
188
|
+
content_length: plaintext_upload_bytes.bytesize,
|
189
|
+
checksum: Digest::MD5.base64digest(plaintext_upload_bytes))
|
190
|
+
|
191
|
+
assert_equal should_be_headers.sort, headers.sort
|
192
|
+
|
193
|
+
res = Net::HTTP.put(URI(url), plaintext_upload_bytes, headers)
|
194
|
+
assert_equal "200", res.code
|
195
|
+
|
196
|
+
assert_equal plaintext_upload_bytes, @service.download(key, encryption_key:)
|
197
|
+
|
198
|
+
@service.delete(key)
|
199
|
+
refute @service.exist?(key)
|
200
|
+
end
|
201
|
+
end
|
@@ -235,7 +235,10 @@ class ActiveStorageEncryption::EncryptedS3ServiceTest < ActiveSupport::TestCase
|
|
235
235
|
# Read the objects from something slow, so that threads may switch between one another
|
236
236
|
class SnoozyStringIO < StringIO
|
237
237
|
def read(n = nil, outbuf = nil)
|
238
|
-
|
238
|
+
sleep_from = 0.1
|
239
|
+
sleep_to = 0.2
|
240
|
+
delay_s = rand(sleep_from..sleep_to)
|
241
|
+
sleep(delay_s)
|
239
242
|
super
|
240
243
|
end
|
241
244
|
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class ActiveStorageEncryption::OverridesTest < ActiveSupport::TestCase
|
6
|
+
include ActiveJob::TestHelper
|
7
|
+
|
8
|
+
setup do
|
9
|
+
ActiveStorage::Current.url_options = {
|
10
|
+
host: "www.example.com",
|
11
|
+
protocol: "https"
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_encryption_key_is_set_on_encrypted_service_before_saving
|
16
|
+
blob = ActiveStorage::Blob.create!(
|
17
|
+
key: "yoyo",
|
18
|
+
filename: "test.txt",
|
19
|
+
byte_size: 10,
|
20
|
+
checksum: "abab",
|
21
|
+
metadata: {"identified" => true},
|
22
|
+
content_type: "text/plain",
|
23
|
+
encryption_key: "blabla",
|
24
|
+
service_name: "encrypted_disk"
|
25
|
+
)
|
26
|
+
|
27
|
+
assert blob.valid?
|
28
|
+
blob.encryption_key = nil
|
29
|
+
refute blob.valid?
|
30
|
+
assert_equal ["Encryption key must be present for this service"], blob.errors.full_messages
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_attach_download_and_destroy_with_encryption_works
|
34
|
+
user = User.create!
|
35
|
+
with_uploadable_random_file do |file|
|
36
|
+
user.file.attach(io: file, filename: "test.txt")
|
37
|
+
end
|
38
|
+
assert user.file.url.include?("/active-storage-encryption/blob/")
|
39
|
+
assert user.file.blob.encryption_key
|
40
|
+
with_uploadable_random_file do |file|
|
41
|
+
assert_equal file.size, user.file.blob.byte_size
|
42
|
+
end
|
43
|
+
with_uploadable_random_file do |file|
|
44
|
+
assert_equal file.read, user.file.download
|
45
|
+
end
|
46
|
+
user.file.destroy
|
47
|
+
user.reload
|
48
|
+
refute user.file.attached?
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_generate_random_encryption_key_is_long_enough
|
52
|
+
key = ActiveStorage::Blob.generate_random_encryption_key
|
53
|
+
assert_equal 48, key.size
|
54
|
+
assert_equal Encoding::BINARY, key.encoding
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_service_encrypted
|
58
|
+
blob = ActiveStorage::Blob.create!(service_name: :encrypted_disk, checksum: "yoyo", encryption_key: "haha", filename: "test", key: "hahahaha", byte_size: 50)
|
59
|
+
assert blob.service_encrypted?
|
60
|
+
blob_2 = ActiveStorage::Blob.create!(service_name: :test, checksum: "yoyo", filename: "test", key: "ok", byte_size: 50)
|
61
|
+
refute blob_2.service_encrypted?
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_create_before_direct_upload_works_with_encryption_and_without
|
65
|
+
blob = with_uploadable_random_file do |file|
|
66
|
+
ActiveStorage::Blob.create_before_direct_upload!(
|
67
|
+
filename: "test_upload",
|
68
|
+
byte_size: file.size,
|
69
|
+
checksum: "something",
|
70
|
+
metadata: {"identified" => true},
|
71
|
+
service_name: "encrypted_disk"
|
72
|
+
)
|
73
|
+
end
|
74
|
+
assert_raises ActiveStorage::FileNotFoundError do
|
75
|
+
blob.download
|
76
|
+
end
|
77
|
+
|
78
|
+
assert blob.service_encrypted?
|
79
|
+
assert blob.encryption_key
|
80
|
+
|
81
|
+
blob_2 = with_uploadable_random_file do |file|
|
82
|
+
ActiveStorage::Blob.create_before_direct_upload!(
|
83
|
+
filename: "test_upload_2",
|
84
|
+
byte_size: file.size,
|
85
|
+
checksum: "something",
|
86
|
+
metadata: {"identified" => true},
|
87
|
+
service_name: "test"
|
88
|
+
)
|
89
|
+
end
|
90
|
+
refute blob_2.service_encrypted?
|
91
|
+
refute blob_2.encryption_key
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_create_and_upload_works_with_encryption_and_without
|
95
|
+
encrypted_blob = with_uploadable_random_file do |file|
|
96
|
+
ActiveStorage::Blob.create_and_upload!(
|
97
|
+
io: file,
|
98
|
+
filename: "test_upload",
|
99
|
+
metadata: {"identified" => true},
|
100
|
+
service_name: "encrypted_disk"
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
assert encrypted_blob.service_encrypted?
|
105
|
+
assert encrypted_blob.url.include?("/active-storage-encryption/blob/")
|
106
|
+
assert encrypted_blob.encryption_key
|
107
|
+
with_uploadable_random_file do |file|
|
108
|
+
assert_equal file.size, encrypted_blob.byte_size
|
109
|
+
end
|
110
|
+
with_uploadable_random_file do |file|
|
111
|
+
assert_equal file.read, encrypted_blob.download
|
112
|
+
end
|
113
|
+
|
114
|
+
unencrypted_blob = with_uploadable_random_file do |file|
|
115
|
+
ActiveStorage::Blob.create_and_upload!(
|
116
|
+
io: file,
|
117
|
+
filename: "test_upload_2",
|
118
|
+
metadata: {"identified" => true},
|
119
|
+
service_name: "test"
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
refute unencrypted_blob.service_encrypted?
|
124
|
+
assert unencrypted_blob.url.include?("/rails/active_storage/disk/")
|
125
|
+
refute unencrypted_blob.encryption_key
|
126
|
+
with_uploadable_random_file do |file|
|
127
|
+
assert_equal file.size, unencrypted_blob.byte_size
|
128
|
+
end
|
129
|
+
with_uploadable_random_file do |file|
|
130
|
+
assert_equal file.read, unencrypted_blob.download
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_open_temp_reads_the_content_of_the_blob_with_encryption_and_without
|
135
|
+
encrypted_blob = with_uploadable_random_file do |file|
|
136
|
+
ActiveStorage::Blob.create_and_upload!(
|
137
|
+
io: file,
|
138
|
+
filename: "test_upload",
|
139
|
+
metadata: {"identified" => true},
|
140
|
+
service_name: "encrypted_disk"
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
with_uploadable_random_file do |file|
|
145
|
+
encrypted_blob.open do |b|
|
146
|
+
assert_equal file.read, b.read
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
unencrypted_blob = with_uploadable_random_file do |file|
|
151
|
+
ActiveStorage::Blob.create_and_upload!(
|
152
|
+
io: file,
|
153
|
+
filename: "test_upload_2",
|
154
|
+
metadata: {"identified" => true},
|
155
|
+
service_name: "test"
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
with_uploadable_random_file do |file|
|
160
|
+
unencrypted_blob.open do |b|
|
161
|
+
assert_equal file.read, b.read
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def test_can_download_a_chunk_with_encryption_and_without
|
167
|
+
encrypted_blob = with_uploadable_random_file do |file|
|
168
|
+
ActiveStorage::Blob.create_and_upload!(
|
169
|
+
io: file,
|
170
|
+
filename: "test_upload",
|
171
|
+
metadata: {"identified" => true},
|
172
|
+
service_name: "encrypted_disk"
|
173
|
+
)
|
174
|
+
end
|
175
|
+
|
176
|
+
chunk = encrypted_blob.download_chunk(0..5.bytes)
|
177
|
+
with_uploadable_random_file do |file|
|
178
|
+
assert_equal chunk, file.read(6)
|
179
|
+
end
|
180
|
+
|
181
|
+
unencrypted_blob = with_uploadable_random_file do |file|
|
182
|
+
ActiveStorage::Blob.create_and_upload!(
|
183
|
+
io: file,
|
184
|
+
filename: "test_upload_2",
|
185
|
+
metadata: {"identified" => true},
|
186
|
+
service_name: "test"
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
chunk = unencrypted_blob.download_chunk(0..5.bytes)
|
191
|
+
with_uploadable_random_file do |file|
|
192
|
+
assert_equal chunk, file.read(6)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_serializable_hash_works_with_encryption_and_without
|
197
|
+
encrypted_blob = with_uploadable_random_file do |file|
|
198
|
+
ActiveStorage::Blob.create_and_upload!(
|
199
|
+
io: file,
|
200
|
+
filename: "test_upload",
|
201
|
+
metadata: {"identified" => true},
|
202
|
+
service_name: "encrypted_disk"
|
203
|
+
)
|
204
|
+
end
|
205
|
+
encrypted_blob_hash = {
|
206
|
+
"id" => encrypted_blob.id,
|
207
|
+
"key" => encrypted_blob.key,
|
208
|
+
"filename" => encrypted_blob.filename,
|
209
|
+
"content_type" => encrypted_blob.content_type,
|
210
|
+
"metadata" => encrypted_blob.metadata,
|
211
|
+
"service_name" => encrypted_blob.service_name,
|
212
|
+
"byte_size" => encrypted_blob.byte_size,
|
213
|
+
"checksum" => encrypted_blob.checksum,
|
214
|
+
"created_at" => encrypted_blob.created_at
|
215
|
+
}
|
216
|
+
assert_equal encrypted_blob_hash.sort, encrypted_blob.serializable_hash.sort
|
217
|
+
|
218
|
+
unencrypted_blob = with_uploadable_random_file do |file|
|
219
|
+
ActiveStorage::Blob.create_and_upload!(
|
220
|
+
io: file,
|
221
|
+
filename: "test_upload_2",
|
222
|
+
metadata: {"identified" => true},
|
223
|
+
service_name: "encrypted_disk"
|
224
|
+
)
|
225
|
+
end
|
226
|
+
unencrypted_blob_hash = {
|
227
|
+
"id" => unencrypted_blob.id,
|
228
|
+
"key" => unencrypted_blob.key,
|
229
|
+
"filename" => unencrypted_blob.filename,
|
230
|
+
"content_type" => encrypted_blob.content_type,
|
231
|
+
"metadata" => unencrypted_blob.metadata,
|
232
|
+
"service_name" => unencrypted_blob.service_name,
|
233
|
+
"byte_size" => unencrypted_blob.byte_size,
|
234
|
+
"checksum" => unencrypted_blob.checksum,
|
235
|
+
"created_at" => unencrypted_blob.created_at
|
236
|
+
}
|
237
|
+
assert_equal unencrypted_blob_hash, unencrypted_blob.serializable_hash
|
238
|
+
end
|
239
|
+
|
240
|
+
def test_instance_compose_works_with_encryption_and_without
|
241
|
+
rng = Random.new(Minitest.seed)
|
242
|
+
|
243
|
+
encrypted_blob_1 = with_uploadable_random_file do |file|
|
244
|
+
ActiveStorage::Blob.create_and_upload!(
|
245
|
+
io: file, filename: "test_upload", metadata: {"identified" => true},
|
246
|
+
service_name: "encrypted_disk"
|
247
|
+
)
|
248
|
+
end
|
249
|
+
encrypted_blob_2 = with_uploadable_random_file do |file|
|
250
|
+
ActiveStorage::Blob.create_and_upload!(
|
251
|
+
io: file, filename: "test_upload_2", metadata: {"identified" => true},
|
252
|
+
service_name: "encrypted_disk"
|
253
|
+
)
|
254
|
+
end
|
255
|
+
new_encrypted_blob = ActiveStorage::Blob.create_before_direct_upload!(
|
256
|
+
key: "new_blob_key", filename: "combined_test_upload",
|
257
|
+
metadata: {"identified" => true}, content_type: "plain/text",
|
258
|
+
checksum: "okok",
|
259
|
+
byte_size: encrypted_blob_1.byte_size + encrypted_blob_2.byte_size,
|
260
|
+
encryption_key: rng.bytes(68),
|
261
|
+
service_name: "encrypted_disk"
|
262
|
+
)
|
263
|
+
new_encrypted_blob.compose([encrypted_blob_1.key, encrypted_blob_2.key], source_encryption_keys: [encrypted_blob_1.encryption_key, encrypted_blob_2.encryption_key])
|
264
|
+
with_uploadable_random_file do |file|
|
265
|
+
assert_equal file.read * 2, new_encrypted_blob.download
|
266
|
+
end
|
267
|
+
|
268
|
+
unencrypted_blob_1 = with_uploadable_random_file do |file|
|
269
|
+
ActiveStorage::Blob.create_and_upload!(
|
270
|
+
io: file, filename: "test_upload_3", metadata: {"identified" => true},
|
271
|
+
service_name: "test"
|
272
|
+
)
|
273
|
+
end
|
274
|
+
unencrypted_blob_2 = with_uploadable_random_file do |file|
|
275
|
+
ActiveStorage::Blob.create_and_upload!(
|
276
|
+
io: file, filename: "test_upload_4", metadata: {"identified" => true},
|
277
|
+
service_name: "test"
|
278
|
+
)
|
279
|
+
end
|
280
|
+
new_unencrypted_blob = ActiveStorage::Blob.create_before_direct_upload!(
|
281
|
+
key: "new_blob_key_2", filename: "combined_test_upload",
|
282
|
+
metadata: {"identified" => true}, content_type: "plain/text",
|
283
|
+
checksum: "okok",
|
284
|
+
byte_size: unencrypted_blob_1.byte_size + unencrypted_blob_2.byte_size
|
285
|
+
)
|
286
|
+
new_unencrypted_blob.compose([unencrypted_blob_1.key, unencrypted_blob_2.key])
|
287
|
+
with_uploadable_random_file do |file|
|
288
|
+
assert_equal file.read * 2, new_unencrypted_blob.download
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def test_class_compose_works_with_and_without_encryption
|
293
|
+
rng = Random.new(Minitest.seed)
|
294
|
+
|
295
|
+
encrypted_blob_1 = with_uploadable_random_file do |file|
|
296
|
+
ActiveStorage::Blob.create_and_upload!(
|
297
|
+
io: file, filename: "test_upload", metadata: {"identified" => true},
|
298
|
+
service_name: "encrypted_disk"
|
299
|
+
)
|
300
|
+
end
|
301
|
+
encrypted_blob_2 = with_uploadable_random_file do |file|
|
302
|
+
ActiveStorage::Blob.create_and_upload!(
|
303
|
+
io: file, filename: "test_upload_2", metadata: {"identified" => true},
|
304
|
+
service_name: "encrypted_disk"
|
305
|
+
)
|
306
|
+
end
|
307
|
+
new_enc_blob = ActiveStorage::Blob.compose(
|
308
|
+
[encrypted_blob_1, encrypted_blob_2],
|
309
|
+
content_type: "text/plain",
|
310
|
+
filename: "composed_blob",
|
311
|
+
metadata: {"identified" => true},
|
312
|
+
key: "new_enc_blob",
|
313
|
+
service_name: "encrypted_disk",
|
314
|
+
encryption_key: rng.bytes(68)
|
315
|
+
)
|
316
|
+
with_uploadable_random_file do |file|
|
317
|
+
assert_equal file.read * 2, new_enc_blob.download
|
318
|
+
end
|
319
|
+
|
320
|
+
unencrypted_blob_1 = with_uploadable_random_file do |file|
|
321
|
+
ActiveStorage::Blob.create_and_upload!(
|
322
|
+
io: file, filename: "test_upload_3", metadata: {"identified" => true},
|
323
|
+
service_name: "test"
|
324
|
+
)
|
325
|
+
end
|
326
|
+
unencrypted_blob_2 = with_uploadable_random_file do |file|
|
327
|
+
ActiveStorage::Blob.create_and_upload!(
|
328
|
+
io: file, filename: "test_upload_4", metadata: {"identified" => true},
|
329
|
+
service_name: "test"
|
330
|
+
)
|
331
|
+
end
|
332
|
+
new_unencrypted_blob = ActiveStorage::Blob.compose(
|
333
|
+
[unencrypted_blob_1, unencrypted_blob_2],
|
334
|
+
key: "new_unencblob_key_2", filename: "combined_test_upload_2",
|
335
|
+
metadata: {"identified" => true}, service_name: "test"
|
336
|
+
)
|
337
|
+
with_uploadable_random_file do |file|
|
338
|
+
assert_equal file.read * 2, new_unencrypted_blob.download
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
private
|
343
|
+
|
344
|
+
def with_uploadable_random_file(size = 128, &blk)
|
345
|
+
rng = Random.new(Minitest.seed)
|
346
|
+
plaintext_upload_bytes = rng.bytes(size)
|
347
|
+
StringIO.open(plaintext_upload_bytes, "rb", &blk)
|
348
|
+
end
|
349
|
+
end
|