aws-sdk-s3 1.48.0 → 1.183.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 +5 -5
- data/CHANGELOG.md +1352 -0
- data/LICENSE.txt +202 -0
- data/VERSION +1 -0
- data/lib/aws-sdk-s3/access_grants_credentials.rb +57 -0
- data/lib/aws-sdk-s3/access_grants_credentials_provider.rb +250 -0
- data/lib/aws-sdk-s3/bucket.rb +1005 -106
- data/lib/aws-sdk-s3/bucket_acl.rb +65 -18
- data/lib/aws-sdk-s3/bucket_cors.rb +80 -18
- data/lib/aws-sdk-s3/bucket_lifecycle.rb +71 -20
- data/lib/aws-sdk-s3/bucket_lifecycle_configuration.rb +126 -21
- data/lib/aws-sdk-s3/bucket_logging.rb +68 -16
- data/lib/aws-sdk-s3/bucket_notification.rb +52 -20
- data/lib/aws-sdk-s3/bucket_policy.rb +108 -17
- data/lib/aws-sdk-s3/bucket_region_cache.rb +11 -5
- data/lib/aws-sdk-s3/bucket_request_payment.rb +60 -15
- data/lib/aws-sdk-s3/bucket_tagging.rb +71 -18
- data/lib/aws-sdk-s3/bucket_versioning.rb +133 -17
- data/lib/aws-sdk-s3/bucket_website.rb +78 -21
- data/lib/aws-sdk-s3/client.rb +14517 -941
- data/lib/aws-sdk-s3/client_api.rb +1296 -197
- data/lib/aws-sdk-s3/customizations/bucket.rb +56 -37
- data/lib/aws-sdk-s3/customizations/errors.rb +40 -0
- data/lib/aws-sdk-s3/customizations/multipart_upload.rb +2 -0
- data/lib/aws-sdk-s3/customizations/object.rb +288 -68
- data/lib/aws-sdk-s3/customizations/object_summary.rb +10 -0
- data/lib/aws-sdk-s3/customizations/object_version.rb +13 -0
- data/lib/aws-sdk-s3/customizations/types/list_object_versions_output.rb +2 -0
- data/lib/aws-sdk-s3/customizations/types/permanent_redirect.rb +26 -0
- data/lib/aws-sdk-s3/customizations.rb +27 -28
- data/lib/aws-sdk-s3/encryption/client.rb +28 -7
- data/lib/aws-sdk-s3/encryption/decrypt_handler.rb +71 -29
- data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +43 -5
- data/lib/aws-sdk-s3/encryption/default_key_provider.rb +2 -0
- data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +13 -2
- data/lib/aws-sdk-s3/encryption/errors.rb +2 -0
- data/lib/aws-sdk-s3/encryption/io_auth_decrypter.rb +2 -0
- data/lib/aws-sdk-s3/encryption/io_decrypter.rb +11 -3
- data/lib/aws-sdk-s3/encryption/io_encrypter.rb +2 -0
- data/lib/aws-sdk-s3/encryption/key_provider.rb +2 -0
- data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +46 -11
- data/lib/aws-sdk-s3/encryption/materials.rb +8 -6
- data/lib/aws-sdk-s3/encryption/utils.rb +25 -0
- data/lib/aws-sdk-s3/encryption.rb +4 -0
- data/lib/aws-sdk-s3/encryptionV2/client.rb +570 -0
- data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +223 -0
- data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +170 -0
- data/lib/aws-sdk-s3/encryptionV2/default_key_provider.rb +40 -0
- data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +65 -0
- data/lib/aws-sdk-s3/encryptionV2/errors.rb +37 -0
- data/lib/aws-sdk-s3/encryptionV2/io_auth_decrypter.rb +58 -0
- data/lib/aws-sdk-s3/encryptionV2/io_decrypter.rb +37 -0
- data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +73 -0
- data/lib/aws-sdk-s3/encryptionV2/key_provider.rb +31 -0
- data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +173 -0
- data/lib/aws-sdk-s3/encryptionV2/materials.rb +60 -0
- data/lib/aws-sdk-s3/encryptionV2/utils.rb +103 -0
- data/lib/aws-sdk-s3/encryption_v2.rb +23 -0
- data/lib/aws-sdk-s3/endpoint_parameters.rb +181 -0
- data/lib/aws-sdk-s3/endpoint_provider.rb +716 -0
- data/lib/aws-sdk-s3/endpoints.rb +1434 -0
- data/lib/aws-sdk-s3/errors.rb +170 -1
- data/lib/aws-sdk-s3/event_streams.rb +8 -1
- data/lib/aws-sdk-s3/express_credentials.rb +55 -0
- data/lib/aws-sdk-s3/express_credentials_provider.rb +59 -0
- data/lib/aws-sdk-s3/file_downloader.rb +161 -46
- data/lib/aws-sdk-s3/file_part.rb +11 -6
- data/lib/aws-sdk-s3/file_uploader.rb +39 -18
- data/lib/aws-sdk-s3/legacy_signer.rb +17 -25
- data/lib/aws-sdk-s3/multipart_file_uploader.rb +104 -27
- data/lib/aws-sdk-s3/multipart_stream_uploader.rb +61 -21
- data/lib/aws-sdk-s3/multipart_upload.rb +342 -32
- data/lib/aws-sdk-s3/multipart_upload_error.rb +2 -0
- data/lib/aws-sdk-s3/multipart_upload_part.rb +384 -46
- data/lib/aws-sdk-s3/object.rb +2600 -231
- data/lib/aws-sdk-s3/object_acl.rb +103 -25
- data/lib/aws-sdk-s3/object_copier.rb +9 -5
- data/lib/aws-sdk-s3/object_multipart_copier.rb +48 -22
- data/lib/aws-sdk-s3/object_summary.rb +2174 -204
- data/lib/aws-sdk-s3/object_version.rb +539 -80
- data/lib/aws-sdk-s3/plugins/accelerate.rb +17 -64
- data/lib/aws-sdk-s3/plugins/access_grants.rb +178 -0
- data/lib/aws-sdk-s3/plugins/arn.rb +70 -0
- data/lib/aws-sdk-s3/plugins/bucket_dns.rb +7 -43
- data/lib/aws-sdk-s3/plugins/bucket_name_restrictions.rb +20 -3
- data/lib/aws-sdk-s3/plugins/checksum_algorithm.rb +31 -0
- data/lib/aws-sdk-s3/plugins/dualstack.rb +7 -50
- data/lib/aws-sdk-s3/plugins/endpoints.rb +86 -0
- data/lib/aws-sdk-s3/plugins/expect_100_continue.rb +5 -4
- data/lib/aws-sdk-s3/plugins/express_session_auth.rb +88 -0
- data/lib/aws-sdk-s3/plugins/get_bucket_location_fix.rb +3 -1
- data/lib/aws-sdk-s3/plugins/http_200_errors.rb +62 -17
- data/lib/aws-sdk-s3/plugins/iad_regional_endpoint.rb +44 -0
- data/lib/aws-sdk-s3/plugins/location_constraint.rb +5 -1
- data/lib/aws-sdk-s3/plugins/md5s.rb +14 -70
- data/lib/aws-sdk-s3/plugins/redirects.rb +2 -0
- data/lib/aws-sdk-s3/plugins/s3_host_id.rb +2 -0
- data/lib/aws-sdk-s3/plugins/s3_signer.rb +63 -94
- data/lib/aws-sdk-s3/plugins/sse_cpk.rb +3 -1
- data/lib/aws-sdk-s3/plugins/streaming_retry.rb +139 -0
- data/lib/aws-sdk-s3/plugins/url_encoded_keys.rb +2 -0
- data/lib/aws-sdk-s3/presigned_post.rb +160 -99
- data/lib/aws-sdk-s3/presigner.rb +141 -62
- data/lib/aws-sdk-s3/resource.rb +156 -17
- data/lib/aws-sdk-s3/types.rb +13021 -4106
- data/lib/aws-sdk-s3/waiters.rb +67 -1
- data/lib/aws-sdk-s3.rb +46 -32
- data/sig/bucket.rbs +222 -0
- data/sig/bucket_acl.rbs +78 -0
- data/sig/bucket_cors.rbs +69 -0
- data/sig/bucket_lifecycle.rbs +88 -0
- data/sig/bucket_lifecycle_configuration.rbs +115 -0
- data/sig/bucket_logging.rbs +76 -0
- data/sig/bucket_notification.rbs +114 -0
- data/sig/bucket_policy.rbs +59 -0
- data/sig/bucket_request_payment.rbs +54 -0
- data/sig/bucket_tagging.rbs +65 -0
- data/sig/bucket_versioning.rbs +77 -0
- data/sig/bucket_website.rbs +93 -0
- data/sig/client.rbs +2472 -0
- data/sig/customizations/bucket.rbs +19 -0
- data/sig/customizations/object.rbs +38 -0
- data/sig/customizations/object_summary.rbs +35 -0
- data/sig/errors.rbs +42 -0
- data/sig/multipart_upload.rbs +120 -0
- data/sig/multipart_upload_part.rbs +109 -0
- data/sig/object.rbs +459 -0
- data/sig/object_acl.rbs +86 -0
- data/sig/object_summary.rbs +345 -0
- data/sig/object_version.rbs +143 -0
- data/sig/resource.rbs +134 -0
- data/sig/types.rbs +2712 -0
- data/sig/waiters.rbs +95 -0
- metadata +74 -15
@@ -0,0 +1,223 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Aws
|
6
|
+
module S3
|
7
|
+
module EncryptionV2
|
8
|
+
# @api private
|
9
|
+
class DecryptHandler < Seahorse::Client::Handler
|
10
|
+
@@warned_response_target_proc = false
|
11
|
+
|
12
|
+
V1_ENVELOPE_KEYS = %w(
|
13
|
+
x-amz-key
|
14
|
+
x-amz-iv
|
15
|
+
x-amz-matdesc
|
16
|
+
)
|
17
|
+
|
18
|
+
V2_ENVELOPE_KEYS = %w(
|
19
|
+
x-amz-key-v2
|
20
|
+
x-amz-iv
|
21
|
+
x-amz-cek-alg
|
22
|
+
x-amz-wrap-alg
|
23
|
+
x-amz-matdesc
|
24
|
+
)
|
25
|
+
|
26
|
+
V2_OPTIONAL_KEYS = %w(x-amz-tag-len)
|
27
|
+
|
28
|
+
POSSIBLE_ENVELOPE_KEYS = (V1_ENVELOPE_KEYS +
|
29
|
+
V2_ENVELOPE_KEYS + V2_OPTIONAL_KEYS).uniq
|
30
|
+
|
31
|
+
POSSIBLE_WRAPPING_FORMATS = %w(
|
32
|
+
AES/GCM
|
33
|
+
kms
|
34
|
+
kms+context
|
35
|
+
RSA-OAEP-SHA1
|
36
|
+
)
|
37
|
+
|
38
|
+
POSSIBLE_ENCRYPTION_FORMATS = %w(
|
39
|
+
AES/GCM/NoPadding
|
40
|
+
AES/CBC/PKCS5Padding
|
41
|
+
AES/CBC/PKCS7Padding
|
42
|
+
)
|
43
|
+
|
44
|
+
AUTH_REQUIRED_CEK_ALGS = %w(AES/GCM/NoPadding)
|
45
|
+
|
46
|
+
def call(context)
|
47
|
+
attach_http_event_listeners(context)
|
48
|
+
apply_cse_user_agent(context)
|
49
|
+
|
50
|
+
if context[:response_target].is_a?(Proc) && !@@warned_response_target_proc
|
51
|
+
@@warned_response_target_proc = true
|
52
|
+
warn(':response_target is a Proc, or a block was provided. ' \
|
53
|
+
'Read the entire object to the ' \
|
54
|
+
'end before you start using the decrypted data. This is to ' \
|
55
|
+
'verify that the object has not been modified since it ' \
|
56
|
+
'was encrypted.')
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
@handler.call(context)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def attach_http_event_listeners(context)
|
66
|
+
|
67
|
+
context.http_response.on_headers(200) do
|
68
|
+
cipher, envelope = decryption_cipher(context)
|
69
|
+
decrypter = body_contains_auth_tag?(envelope) ?
|
70
|
+
authenticated_decrypter(context, cipher, envelope) :
|
71
|
+
IODecrypter.new(cipher, context.http_response.body)
|
72
|
+
context.http_response.body = decrypter
|
73
|
+
end
|
74
|
+
|
75
|
+
context.http_response.on_success(200) do
|
76
|
+
decrypter = context.http_response.body
|
77
|
+
decrypter.finalize
|
78
|
+
decrypter.io.rewind if decrypter.io.respond_to?(:rewind)
|
79
|
+
context.http_response.body = decrypter.io
|
80
|
+
end
|
81
|
+
|
82
|
+
context.http_response.on_error do
|
83
|
+
if context.http_response.body.respond_to?(:io)
|
84
|
+
context.http_response.body = context.http_response.body.io
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def decryption_cipher(context)
|
90
|
+
if (envelope = get_encryption_envelope(context))
|
91
|
+
cipher = context[:encryption][:cipher_provider]
|
92
|
+
.decryption_cipher(
|
93
|
+
envelope,
|
94
|
+
context[:encryption]
|
95
|
+
)
|
96
|
+
[cipher, envelope]
|
97
|
+
else
|
98
|
+
raise Errors::DecryptionError, "unable to locate encryption envelope"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_encryption_envelope(context)
|
103
|
+
if context[:encryption][:envelope_location] == :metadata
|
104
|
+
envelope_from_metadata(context) || envelope_from_instr_file(context)
|
105
|
+
else
|
106
|
+
envelope_from_instr_file(context) || envelope_from_metadata(context)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def envelope_from_metadata(context)
|
111
|
+
possible_envelope = {}
|
112
|
+
POSSIBLE_ENVELOPE_KEYS.each do |suffix|
|
113
|
+
if value = context.http_response.headers["x-amz-meta-#{suffix}"]
|
114
|
+
possible_envelope[suffix] = value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
extract_envelope(possible_envelope)
|
118
|
+
end
|
119
|
+
|
120
|
+
def envelope_from_instr_file(context)
|
121
|
+
suffix = context[:encryption][:instruction_file_suffix]
|
122
|
+
possible_envelope = Json.load(context.client.get_object(
|
123
|
+
bucket: context.params[:bucket],
|
124
|
+
key: context.params[:key] + suffix
|
125
|
+
).body.read)
|
126
|
+
extract_envelope(possible_envelope)
|
127
|
+
rescue S3::Errors::ServiceError, Json::ParseError
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
|
131
|
+
def extract_envelope(hash)
|
132
|
+
return nil unless hash
|
133
|
+
return v1_envelope(hash) if hash.key?('x-amz-key')
|
134
|
+
return v2_envelope(hash) if hash.key?('x-amz-key-v2')
|
135
|
+
if hash.keys.any? { |key| key.match(/^x-amz-key-(.+)$/) }
|
136
|
+
msg = "unsupported envelope encryption version #{$1}"
|
137
|
+
raise Errors::DecryptionError, msg
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def v1_envelope(envelope)
|
142
|
+
envelope
|
143
|
+
end
|
144
|
+
|
145
|
+
def v2_envelope(envelope)
|
146
|
+
unless POSSIBLE_ENCRYPTION_FORMATS.include? envelope['x-amz-cek-alg']
|
147
|
+
alg = envelope['x-amz-cek-alg'].inspect
|
148
|
+
msg = "unsupported content encrypting key (cek) format: #{alg}"
|
149
|
+
raise Errors::DecryptionError, msg
|
150
|
+
end
|
151
|
+
unless POSSIBLE_WRAPPING_FORMATS.include? envelope['x-amz-wrap-alg']
|
152
|
+
alg = envelope['x-amz-wrap-alg'].inspect
|
153
|
+
msg = "unsupported key wrapping algorithm: #{alg}"
|
154
|
+
raise Errors::DecryptionError, msg
|
155
|
+
end
|
156
|
+
unless (missing_keys = V2_ENVELOPE_KEYS - envelope.keys).empty?
|
157
|
+
msg = "incomplete v2 encryption envelope:\n"
|
158
|
+
msg += " missing: #{missing_keys.join(',')}\n"
|
159
|
+
raise Errors::DecryptionError, msg
|
160
|
+
end
|
161
|
+
envelope
|
162
|
+
end
|
163
|
+
|
164
|
+
# This method fetches the tag from the end of the object by
|
165
|
+
# making a GET Object w/range request. This auth tag is used
|
166
|
+
# to initialize the cipher, and the decrypter truncates the
|
167
|
+
# auth tag from the body when writing the final bytes.
|
168
|
+
def authenticated_decrypter(context, cipher, envelope)
|
169
|
+
http_resp = context.http_response
|
170
|
+
content_length = http_resp.headers['content-length'].to_i
|
171
|
+
auth_tag_length = auth_tag_length(envelope)
|
172
|
+
|
173
|
+
auth_tag = context.client.get_object(
|
174
|
+
bucket: context.params[:bucket],
|
175
|
+
key: context.params[:key],
|
176
|
+
version_id: context.params[:version_id],
|
177
|
+
range: "bytes=-#{auth_tag_length}"
|
178
|
+
).body.read
|
179
|
+
|
180
|
+
cipher.auth_tag = auth_tag
|
181
|
+
cipher.auth_data = ''
|
182
|
+
|
183
|
+
# The encrypted object contains both the cipher text
|
184
|
+
# plus a trailing auth tag.
|
185
|
+
IOAuthDecrypter.new(
|
186
|
+
io: http_resp.body,
|
187
|
+
encrypted_content_length: content_length - auth_tag_length,
|
188
|
+
cipher: cipher)
|
189
|
+
end
|
190
|
+
|
191
|
+
def body_contains_auth_tag?(envelope)
|
192
|
+
AUTH_REQUIRED_CEK_ALGS.include?(envelope['x-amz-cek-alg'])
|
193
|
+
end
|
194
|
+
|
195
|
+
# Determine the auth tag length from the algorithm
|
196
|
+
# Validate it against the value provided in the x-amz-tag-len
|
197
|
+
# Return the tag length in bytes
|
198
|
+
def auth_tag_length(envelope)
|
199
|
+
tag_length =
|
200
|
+
case envelope['x-amz-cek-alg']
|
201
|
+
when 'AES/GCM/NoPadding' then AES_GCM_TAG_LEN_BYTES
|
202
|
+
else
|
203
|
+
raise ArgumentError, 'Unsupported cek-alg: ' \
|
204
|
+
"#{envelope['x-amz-cek-alg']}"
|
205
|
+
end
|
206
|
+
if (tag_length * 8) != envelope['x-amz-tag-len'].to_i
|
207
|
+
raise Errors::DecryptionError, 'x-amz-tag-len does not match expected'
|
208
|
+
end
|
209
|
+
tag_length
|
210
|
+
end
|
211
|
+
|
212
|
+
def apply_cse_user_agent(context)
|
213
|
+
if context.config.user_agent_suffix.nil?
|
214
|
+
context.config.user_agent_suffix = EC_USER_AGENT
|
215
|
+
elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
|
216
|
+
context.config.user_agent_suffix += " #{EC_USER_AGENT}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Aws
|
6
|
+
module S3
|
7
|
+
module EncryptionV2
|
8
|
+
# @api private
|
9
|
+
class DefaultCipherProvider
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@key_provider = options[:key_provider]
|
13
|
+
@key_wrap_schema = validate_key_wrap(
|
14
|
+
options[:key_wrap_schema],
|
15
|
+
@key_provider.encryption_materials.key
|
16
|
+
)
|
17
|
+
@content_encryption_schema = validate_cek(
|
18
|
+
options[:content_encryption_schema]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Array<Hash,Cipher>] Creates an returns a new encryption
|
23
|
+
# envelope and encryption cipher.
|
24
|
+
def encryption_cipher(options = {})
|
25
|
+
validate_options(options)
|
26
|
+
cipher = Utils.aes_encryption_cipher(:GCM)
|
27
|
+
if @key_provider.encryption_materials.key.is_a? OpenSSL::PKey::RSA
|
28
|
+
enc_key = encode64(
|
29
|
+
encrypt_rsa(envelope_key(cipher), @content_encryption_schema)
|
30
|
+
)
|
31
|
+
else
|
32
|
+
enc_key = encode64(
|
33
|
+
encrypt_aes_gcm(envelope_key(cipher), @content_encryption_schema)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
envelope = {
|
37
|
+
'x-amz-key-v2' => enc_key,
|
38
|
+
'x-amz-cek-alg' => @content_encryption_schema,
|
39
|
+
'x-amz-tag-len' => (AES_GCM_TAG_LEN_BYTES * 8).to_s,
|
40
|
+
'x-amz-wrap-alg' => @key_wrap_schema,
|
41
|
+
'x-amz-iv' => encode64(envelope_iv(cipher)),
|
42
|
+
'x-amz-matdesc' => materials_description
|
43
|
+
}
|
44
|
+
cipher.auth_data = '' # auth_data must be set after key and iv
|
45
|
+
[envelope, cipher]
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Cipher] Given an encryption envelope, returns a
|
49
|
+
# decryption cipher.
|
50
|
+
def decryption_cipher(envelope, options = {})
|
51
|
+
validate_options(options)
|
52
|
+
master_key = @key_provider.key_for(envelope['x-amz-matdesc'])
|
53
|
+
if envelope.key? 'x-amz-key'
|
54
|
+
unless options[:security_profile] == :v2_and_legacy
|
55
|
+
raise Errors::LegacyDecryptionError
|
56
|
+
end
|
57
|
+
# Support for decryption of legacy objects
|
58
|
+
key = Utils.decrypt(master_key, decode64(envelope['x-amz-key']))
|
59
|
+
iv = decode64(envelope['x-amz-iv'])
|
60
|
+
Utils.aes_decryption_cipher(:CBC, key, iv)
|
61
|
+
else
|
62
|
+
if envelope['x-amz-cek-alg'] != 'AES/GCM/NoPadding'
|
63
|
+
raise ArgumentError, 'Unsupported cek-alg: ' \
|
64
|
+
"#{envelope['x-amz-cek-alg']}"
|
65
|
+
end
|
66
|
+
key =
|
67
|
+
case envelope['x-amz-wrap-alg']
|
68
|
+
when 'AES/GCM'
|
69
|
+
if master_key.is_a? OpenSSL::PKey::RSA
|
70
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
71
|
+
' with an RSA key and the x-amz-wrap-alg is AES/GCM.'
|
72
|
+
end
|
73
|
+
Utils.decrypt_aes_gcm(master_key,
|
74
|
+
decode64(envelope['x-amz-key-v2']),
|
75
|
+
envelope['x-amz-cek-alg'])
|
76
|
+
when 'RSA-OAEP-SHA1'
|
77
|
+
unless master_key.is_a? OpenSSL::PKey::RSA
|
78
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
79
|
+
' with an AES key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
|
80
|
+
end
|
81
|
+
key, cek_alg = Utils.decrypt_rsa(master_key, decode64(envelope['x-amz-key-v2']))
|
82
|
+
raise Errors::CEKAlgMismatchError unless cek_alg == envelope['x-amz-cek-alg']
|
83
|
+
key
|
84
|
+
when 'kms+context'
|
85
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
86
|
+
' with a user provided key and the x-amz-wrap-alg is' \
|
87
|
+
' kms+context. Please configure the client with the' \
|
88
|
+
' required kms_key_id'
|
89
|
+
else
|
90
|
+
raise ArgumentError, 'Unsupported wrap-alg: ' \
|
91
|
+
"#{envelope['x-amz-wrap-alg']}"
|
92
|
+
end
|
93
|
+
iv = decode64(envelope['x-amz-iv'])
|
94
|
+
Utils.aes_decryption_cipher(:GCM, key, iv)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Validate that the key_wrap_schema
|
101
|
+
# is valid, supported and matches the provided key.
|
102
|
+
# Returns the string version for the x-amz-key-wrap-alg
|
103
|
+
def validate_key_wrap(key_wrap_schema, key)
|
104
|
+
if key.is_a? OpenSSL::PKey::RSA
|
105
|
+
unless key_wrap_schema == :rsa_oaep_sha1
|
106
|
+
raise ArgumentError, ':key_wrap_schema must be set to :rsa_oaep_sha1 for RSA keys.'
|
107
|
+
end
|
108
|
+
else
|
109
|
+
unless key_wrap_schema == :aes_gcm
|
110
|
+
raise ArgumentError, ':key_wrap_schema must be set to :aes_gcm for AES keys.'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
case key_wrap_schema
|
115
|
+
when :rsa_oaep_sha1 then 'RSA-OAEP-SHA1'
|
116
|
+
when :aes_gcm then 'AES/GCM'
|
117
|
+
when :kms_context
|
118
|
+
raise ArgumentError, 'A kms_key_id is required when using :kms_context.'
|
119
|
+
else
|
120
|
+
raise ArgumentError, "Unsupported key_wrap_schema: #{key_wrap_schema}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def validate_cek(content_encryption_schema)
|
125
|
+
case content_encryption_schema
|
126
|
+
when :aes_gcm_no_padding
|
127
|
+
"AES/GCM/NoPadding"
|
128
|
+
else
|
129
|
+
raise ArgumentError, "Unsupported content_encryption_schema: #{content_encryption_schema}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def envelope_key(cipher)
|
134
|
+
cipher.key = cipher.random_key
|
135
|
+
end
|
136
|
+
|
137
|
+
def envelope_iv(cipher)
|
138
|
+
cipher.iv = cipher.random_iv
|
139
|
+
end
|
140
|
+
|
141
|
+
def encrypt_aes_gcm(data, auth_data)
|
142
|
+
Utils.encrypt_aes_gcm(@key_provider.encryption_materials.key, data, auth_data)
|
143
|
+
end
|
144
|
+
|
145
|
+
def encrypt_rsa(data, auth_data)
|
146
|
+
Utils.encrypt_rsa(@key_provider.encryption_materials.key, data, auth_data)
|
147
|
+
end
|
148
|
+
|
149
|
+
def materials_description
|
150
|
+
@key_provider.encryption_materials.description
|
151
|
+
end
|
152
|
+
|
153
|
+
def encode64(str)
|
154
|
+
Base64.encode64(str).split("\n") * ''
|
155
|
+
end
|
156
|
+
|
157
|
+
def decode64(str)
|
158
|
+
Base64.decode64(str)
|
159
|
+
end
|
160
|
+
|
161
|
+
def validate_options(options)
|
162
|
+
if !options[:kms_encryption_context].nil?
|
163
|
+
raise ArgumentError, 'Cannot provide :kms_encryption_context ' \
|
164
|
+
'with non KMS client.'
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
module EncryptionV2
|
6
|
+
|
7
|
+
# The default key provider is constructed with a single key
|
8
|
+
# that is used for both encryption and decryption, ignoring
|
9
|
+
# the possible per-object envelope encryption materials description.
|
10
|
+
# @api private
|
11
|
+
class DefaultKeyProvider
|
12
|
+
|
13
|
+
include KeyProvider
|
14
|
+
|
15
|
+
# @option options [required, OpenSSL::PKey::RSA, String] :encryption_key
|
16
|
+
# The master key to use for encrypting objects.
|
17
|
+
# @option options [String<JSON>] :materials_description ('{}')
|
18
|
+
# A description of the encryption key.
|
19
|
+
def initialize(options = {})
|
20
|
+
@encryption_materials = Materials.new(
|
21
|
+
key: options[:encryption_key],
|
22
|
+
description: options[:materials_description] || '{}'
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Materials]
|
27
|
+
def encryption_materials
|
28
|
+
@encryption_materials
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param [String<JSON>] materials_description
|
32
|
+
# @return Returns the key given in the constructor.
|
33
|
+
def key_for(materials_description)
|
34
|
+
@encryption_materials.key
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Aws
|
6
|
+
module S3
|
7
|
+
module EncryptionV2
|
8
|
+
# @api private
|
9
|
+
class EncryptHandler < Seahorse::Client::Handler
|
10
|
+
|
11
|
+
def call(context)
|
12
|
+
envelope, cipher = context[:encryption][:cipher_provider]
|
13
|
+
.encryption_cipher(
|
14
|
+
kms_encryption_context: context[:encryption][:kms_encryption_context]
|
15
|
+
)
|
16
|
+
context[:encryption][:cipher] = cipher
|
17
|
+
apply_encryption_envelope(context, envelope)
|
18
|
+
apply_encryption_cipher(context, cipher)
|
19
|
+
apply_cse_user_agent(context)
|
20
|
+
@handler.call(context)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def apply_encryption_envelope(context, envelope)
|
26
|
+
if context[:encryption][:envelope_location] == :instruction_file
|
27
|
+
suffix = context[:encryption][:instruction_file_suffix]
|
28
|
+
context.client.put_object(
|
29
|
+
bucket: context.params[:bucket],
|
30
|
+
key: context.params[:key] + suffix,
|
31
|
+
body: Json.dump(envelope)
|
32
|
+
)
|
33
|
+
else # :metadata
|
34
|
+
context.params[:metadata] ||= {}
|
35
|
+
context.params[:metadata].update(envelope)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def apply_encryption_cipher(context, cipher)
|
40
|
+
io = context.params[:body] || ''
|
41
|
+
io = StringIO.new(io) if io.is_a? String
|
42
|
+
context.params[:body] = IOEncrypter.new(cipher, io)
|
43
|
+
context.params[:metadata] ||= {}
|
44
|
+
context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
|
45
|
+
if context.params.delete(:content_md5)
|
46
|
+
raise ArgumentError, 'Setting content_md5 on client side '\
|
47
|
+
'encrypted objects is deprecated.'
|
48
|
+
end
|
49
|
+
context.http_response.on_headers do
|
50
|
+
context.params[:body].close
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def apply_cse_user_agent(context)
|
55
|
+
if context.config.user_agent_suffix.nil?
|
56
|
+
context.config.user_agent_suffix = EC_USER_AGENT
|
57
|
+
elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
|
58
|
+
context.config.user_agent_suffix += " #{EC_USER_AGENT}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
module EncryptionV2
|
6
|
+
module Errors
|
7
|
+
|
8
|
+
# Generic DecryptionError
|
9
|
+
class DecryptionError < RuntimeError; end
|
10
|
+
|
11
|
+
class EncryptionError < RuntimeError; end
|
12
|
+
|
13
|
+
# Raised when attempting to decrypt a legacy (V1) encrypted object
|
14
|
+
# when using a security_profile that does not support it.
|
15
|
+
class LegacyDecryptionError < DecryptionError
|
16
|
+
def initialize(*args)
|
17
|
+
msg = 'The requested object is ' \
|
18
|
+
'encrypted with V1 encryption schemas that have been disabled ' \
|
19
|
+
'by client configuration security_profile = :v2. Retry with ' \
|
20
|
+
':v2_and_legacy or re-encrypt the object.'
|
21
|
+
super(msg)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class CEKAlgMismatchError < DecryptionError
|
26
|
+
def initialize(*args)
|
27
|
+
msg = 'The content encryption algorithm used at encryption time ' \
|
28
|
+
'does not match the algorithm stored for decryption time. ' \
|
29
|
+
'The object may be altered or corrupted.'
|
30
|
+
super(msg)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
module EncryptionV2
|
6
|
+
# @api private
|
7
|
+
class IOAuthDecrypter
|
8
|
+
|
9
|
+
# @option options [required, IO#write] :io
|
10
|
+
# An IO-like object that responds to {#write}.
|
11
|
+
# @option options [required, Integer] :encrypted_content_length
|
12
|
+
# The number of bytes to decrypt from the `:io` object.
|
13
|
+
# This should be the total size of `:io` minus the length of
|
14
|
+
# the cipher auth tag.
|
15
|
+
# @option options [required, OpenSSL::Cipher] :cipher An initialized
|
16
|
+
# cipher that can be used to decrypt the bytes as they are
|
17
|
+
# written to the `:io` object. The cipher should already have
|
18
|
+
# its `#auth_tag` set.
|
19
|
+
def initialize(options = {})
|
20
|
+
@decrypter = IODecrypter.new(options[:cipher], options[:io])
|
21
|
+
@max_bytes = options[:encrypted_content_length]
|
22
|
+
@bytes_written = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def write(chunk)
|
26
|
+
chunk = truncate_chunk(chunk)
|
27
|
+
if chunk.bytesize > 0
|
28
|
+
@bytes_written += chunk.bytesize
|
29
|
+
@decrypter.write(chunk)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def finalize
|
34
|
+
@decrypter.finalize
|
35
|
+
end
|
36
|
+
|
37
|
+
def io
|
38
|
+
@decrypter.io
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def truncate_chunk(chunk)
|
44
|
+
if chunk.bytesize + @bytes_written <= @max_bytes
|
45
|
+
chunk
|
46
|
+
elsif @bytes_written < @max_bytes
|
47
|
+
chunk[0..(@max_bytes - @bytes_written - 1)]
|
48
|
+
else
|
49
|
+
# If the tag was sent over after the full body has been read,
|
50
|
+
# we don't want to accidentally append it.
|
51
|
+
""
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module S3
|
5
|
+
module EncryptionV2
|
6
|
+
# @api private
|
7
|
+
class IODecrypter
|
8
|
+
|
9
|
+
# @param [OpenSSL::Cipher] cipher
|
10
|
+
# @param [IO#write] io An IO-like object that responds to `#write`.
|
11
|
+
def initialize(cipher, io)
|
12
|
+
@cipher = cipher
|
13
|
+
# Ensure that IO is reset between retries
|
14
|
+
@io = io.tap { |io| io.truncate(0) if io.respond_to?(:truncate) }
|
15
|
+
@cipher_buffer = String.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [#write]
|
19
|
+
attr_reader :io
|
20
|
+
|
21
|
+
def write(chunk)
|
22
|
+
# decrypt and write
|
23
|
+
if @cipher.method(:update).arity == 1
|
24
|
+
@io.write(@cipher.update(chunk))
|
25
|
+
else
|
26
|
+
@io.write(@cipher.update(chunk, @cipher_buffer))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def finalize
|
31
|
+
@io.write(@cipher.final)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|