Class: ActiveStorageEncryption::EncryptedS3Service
- Inherits:
-
ActiveStorage::Service::S3Service
- Object
- ActiveStorage::Service::S3Service
- ActiveStorageEncryption::EncryptedS3Service
- Includes:
- PrivateUrlPolicy
- Defined in:
- lib/active_storage_encryption/encrypted_s3_service.rb
Direct Known Subclasses
Constant Summary
Constants included from PrivateUrlPolicy
PrivateUrlPolicy::DEFAULT_POLICY
Instance Method Summary collapse
- #compose(source_keys, destination_key, source_encryption_keys:, encryption_key:, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}) ⇒ Object
- #download(key, encryption_key:, &block) ⇒ Object
- #download_chunk(key, range, encryption_key:) ⇒ Object
- #encrypted? ⇒ Boolean
- #exist?(key) ⇒ Boolean
- #headers_for_direct_upload(key, encryption_key:, **options_for_super) ⇒ Object
- #headers_for_private_download(key, encryption_key:) ⇒ Object
-
#initialize(public: false, **options_for_s3_service_and_private_url_policy) ⇒ EncryptedS3Service
constructor
A new instance of EncryptedS3Service.
- #service_name ⇒ Object
- #upload(*args, encryption_key:, **kwargs) ⇒ Object
- #url_for_direct_upload(key, encryption_key:, **options_for_super) ⇒ Object
Methods included from PrivateUrlPolicy
#private_url_for_streaming_via_controller, #private_url_policy, #private_url_policy=
Constructor Details
#initialize(public: false, **options_for_s3_service_and_private_url_policy) ⇒ EncryptedS3Service
Returns a new instance of EncryptedS3Service.
9 10 11 12 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 9 def initialize(public: false, **) raise ArgumentError, "encrypted files cannot be served via a public URL or a CDN" if public super end |
Instance Method Details
#compose(source_keys, destination_key, source_encryption_keys:, encryption_key:, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}) ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 109 def compose(source_keys, destination_key, source_encryption_keys:, encryption_key:, filename: nil, content_type: nil, disposition: nil, custom_metadata: {}) if source_keys.length != source_encryption_keys.length raise ArgumentError, "With #{source_keys.length} keys to compose there should be exactly as many source_encryption_keys, but got #{source_encryption_keys.length}" end content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename = .merge((encryption_key)) object_for(destination_key).upload_stream( content_type: content_type, content_disposition: content_disposition, part_size: MINIMUM_UPLOAD_PART_SIZE, metadata: , ** ) do |s3_multipart_io| s3_multipart_io.binmode source_keys.zip(source_encryption_keys).each do |(source_key, source_encryption_key)| stream(source_key, encryption_key: source_encryption_key) do |chunk| s3_multipart_io.write(chunk) end end end end |
#download(key, encryption_key:, &block) ⇒ Object
87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 87 def download(key, encryption_key:, &block) if block_given? instrument :streaming_download, key: key do stream(key, encryption_key: encryption_key, &block) end else instrument :download, key: key do object_for(key).get(**(encryption_key)).body.string.force_encoding(Encoding::BINARY) rescue Aws::S3::Errors::NoSuchKey raise ActiveStorage::FileNotFoundError end end end |
#download_chunk(key, range, encryption_key:) ⇒ Object
101 102 103 104 105 106 107 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 101 def download_chunk(key, range, encryption_key:) instrument :download_chunk, key: key, range: range do object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}", **(encryption_key)).body.string.force_encoding(Encoding::BINARY) rescue Aws::S3::Errors::NoSuchKey raise ActiveStorage::FileNotFoundError end end |
#encrypted? ⇒ Boolean
7 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 7 def encrypted? = true |
#exist?(key) ⇒ Boolean
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 30 def exist?(key) # The stock S3Service uses S3::Object#exists? here. That method does # a HEAD request to the S3 bucket under the hood. But there is a problem # with that approach: to get all the metadata attributes of an object on S3 # (which is what the HEAD request should return to you) you need the encryption key. # The interface of the ActiveStorage services does not provide for extra arguments # for `Service#exist?`, so all we would get using that SDK call would be an error. # # But we don't need the object metadata - we need to know is whether the object exists # at all. And this can be done with a GET request instead. We ask S3 to give us the first byte of the # object. S3 will then raise an exception - the exception will be different # depending on whether the object does not exist _or_ the object does exist, but # is encrypted. We can use the distinction between those exceptions to tell # whether the object is there or not. # # There is also a case where the object is not encrypted - in that situation # our single-byte GET request will actually succeed. This also means that the # object exists in the bucket. object_for(key).get(range: "bytes=0-0") # If we get here without an exception - the object exists in the bucket, # but is not encrypted. For example, it was stored using a stock S3Service. true rescue Aws::S3::Errors::InvalidRequest # With this exception S3 tells us that the object exists but we have to furnish # the encryption key (the exception will have a message with "object was stored # using a form of Server Side Encryption..."). true rescue Aws::S3::Errors::NoSuchKey # And this truly means the object is not present false end |
#headers_for_direct_upload(key, encryption_key:, **options_for_super) ⇒ Object
20 21 22 23 24 25 26 27 28 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 20 def headers_for_direct_upload(key, encryption_key:, **) # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html#specifying-s3-c-encryption # This is the same as sse_options but expressed with raw header names = (encryption_key) super(key, **).merge!({ "x-amz-server-side-encryption-customer-key" => Base64.strict_encode64(.fetch(:sse_customer_key)), "x-amz-server-side-encryption-customer-key-MD5" => Digest::MD5.base64digest(.fetch(:sse_customer_key)) }) end |
#headers_for_private_download(key, encryption_key:) ⇒ Object
62 63 64 65 66 67 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 62 def headers_for_private_download(key, encryption_key:, **) = (encryption_key) { "x-amz-server-side-encryption-customer-key" => Base64.strict_encode64(.fetch(:sse_customer_key)) } end |
#service_name ⇒ Object
14 15 16 17 18 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 14 def service_name # ActiveStorage::Service::DiskService => Disk # Overridden because in Rails 8 this is "self.class.name.split("::").third.remove("Service")" self.class.name.split("::").last.remove("Service") end |
#upload(*args, encryption_key:, **kwargs) ⇒ Object
81 82 83 84 85 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 81 def upload(*args, encryption_key:, **kwargs) ((encryption_key)) do super(*args, **kwargs) end end |
#url_for_direct_upload(key, encryption_key:, **options_for_super) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/active_storage_encryption/encrypted_s3_service.rb', line 69 def url_for_direct_upload(key, encryption_key:, **) # With direct upload we need to remove the encryption key itself from # the SDK parameters. Otherwise it does get included in the URL, but that # does not make S3 actually _use_ the value - _and_ it leaks the key. # We _do_ need the key MD5 to be in the signed header params, so that the client can't use an encryption key # it invents by itself - it must use the one we issue it. = (encryption_key).without(:sse_customer_key) () do super(key, **) end end |