aws-sdk-s3 1.75.0 → 1.79.1
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/lib/aws-sdk-s3.rb +2 -2
- data/lib/aws-sdk-s3/bucket.rb +2 -2
- data/lib/aws-sdk-s3/client.rb +145 -119
- data/lib/aws-sdk-s3/encryption.rb +2 -0
- data/lib/aws-sdk-s3/encryption/client.rb +11 -0
- data/lib/aws-sdk-s3/encryption/decrypt_handler.rb +64 -29
- data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +41 -5
- data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +5 -5
- data/lib/aws-sdk-s3/encryption/io_decrypter.rb +7 -6
- data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +32 -3
- data/lib/aws-sdk-s3/encryption/utils.rb +23 -0
- data/lib/aws-sdk-s3/encryptionV2/client.rb +201 -23
- data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +40 -12
- data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +77 -10
- data/lib/aws-sdk-s3/encryptionV2/default_key_provider.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +7 -4
- data/lib/aws-sdk-s3/encryptionV2/errors.rb +24 -0
- data/lib/aws-sdk-s3/encryptionV2/io_auth_decrypter.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/io_decrypter.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/key_provider.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +90 -20
- data/lib/aws-sdk-s3/encryptionV2/materials.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/utils.rb +2 -15
- data/lib/aws-sdk-s3/encryption_v2.rb +4 -1
- data/lib/aws-sdk-s3/multipart_upload_part.rb +1 -1
- data/lib/aws-sdk-s3/object.rb +1 -1
- data/lib/aws-sdk-s3/object_summary.rb +19 -3
- data/lib/aws-sdk-s3/presigned_post.rb +1 -0
- data/lib/aws-sdk-s3/presigner.rb +2 -2
- data/lib/aws-sdk-s3/resource.rb +1 -1
- data/lib/aws-sdk-s3/types.rb +25 -8
- metadata +4 -4
@@ -5,6 +5,10 @@ require 'forwardable'
|
|
5
5
|
module Aws
|
6
6
|
module S3
|
7
7
|
|
8
|
+
# [MAINTENANCE MODE] There is a new version of the Encryption Client.
|
9
|
+
# AWS strongly recommends upgrading to the {Aws::S3::EncryptionV2::Client},
|
10
|
+
# which provides updated data security best practices.
|
11
|
+
# See documentation for {Aws::S3::EncryptionV2::Client}.
|
8
12
|
# Provides an encryption client that encrypts and decrypts data client-side,
|
9
13
|
# storing the encrypted data in Amazon S3.
|
10
14
|
#
|
@@ -229,6 +233,13 @@ module Aws
|
|
229
233
|
@envelope_location = extract_location(options)
|
230
234
|
@instruction_file_suffix = extract_suffix(options)
|
231
235
|
end
|
236
|
+
deprecated :initialize,
|
237
|
+
message:
|
238
|
+
'[MAINTENANCE MODE] This version of the S3 Encryption client is currently in maintenance mode. ' \
|
239
|
+
'AWS strongly recommends upgrading to the Aws::S3::EncryptionV2::Client, ' \
|
240
|
+
'which provides updated data security best practices. ' \
|
241
|
+
'See documentation for Aws::S3::EncryptionV2::Client.'
|
242
|
+
|
232
243
|
|
233
244
|
# @return [S3::Client]
|
234
245
|
attr_reader :client
|
@@ -7,6 +7,7 @@ module Aws
|
|
7
7
|
module Encryption
|
8
8
|
# @api private
|
9
9
|
class DecryptHandler < Seahorse::Client::Handler
|
10
|
+
@@warned_response_target_proc = false
|
10
11
|
|
11
12
|
V1_ENVELOPE_KEYS = %w(
|
12
13
|
x-amz-key
|
@@ -22,7 +23,17 @@ module Aws
|
|
22
23
|
x-amz-matdesc
|
23
24
|
)
|
24
25
|
|
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
|
+
)
|
26
37
|
|
27
38
|
POSSIBLE_ENCRYPTION_FORMATS = %w(
|
28
39
|
AES/GCM/NoPadding
|
@@ -30,9 +41,21 @@ module Aws
|
|
30
41
|
AES/CBC/PKCS7Padding
|
31
42
|
)
|
32
43
|
|
44
|
+
AUTH_REQUIRED_CEK_ALGS = %w(AES/GCM/NoPadding)
|
45
|
+
|
33
46
|
def call(context)
|
34
47
|
attach_http_event_listeners(context)
|
35
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
|
+
end
|
58
|
+
|
36
59
|
@handler.call(context)
|
37
60
|
end
|
38
61
|
|
@@ -41,9 +64,9 @@ module Aws
|
|
41
64
|
def attach_http_event_listeners(context)
|
42
65
|
|
43
66
|
context.http_response.on_headers(200) do
|
44
|
-
cipher = decryption_cipher(context)
|
45
|
-
decrypter = body_contains_auth_tag?(
|
46
|
-
authenticated_decrypter(context, cipher) :
|
67
|
+
cipher, envelope = decryption_cipher(context)
|
68
|
+
decrypter = body_contains_auth_tag?(envelope) ?
|
69
|
+
authenticated_decrypter(context, cipher, envelope) :
|
47
70
|
IODecrypter.new(cipher, context.http_response.body)
|
48
71
|
context.http_response.body = decrypter
|
49
72
|
end
|
@@ -63,8 +86,13 @@ module Aws
|
|
63
86
|
end
|
64
87
|
|
65
88
|
def decryption_cipher(context)
|
66
|
-
if envelope = get_encryption_envelope(context)
|
67
|
-
context[:encryption][:cipher_provider]
|
89
|
+
if (envelope = get_encryption_envelope(context))
|
90
|
+
cipher = context[:encryption][:cipher_provider]
|
91
|
+
.decryption_cipher(
|
92
|
+
envelope,
|
93
|
+
context[:encryption]
|
94
|
+
)
|
95
|
+
[cipher, envelope]
|
68
96
|
else
|
69
97
|
raise Errors::DecryptionError, "unable to locate encryption envelope"
|
70
98
|
end
|
@@ -100,13 +128,12 @@ module Aws
|
|
100
128
|
end
|
101
129
|
|
102
130
|
def extract_envelope(hash)
|
131
|
+
return nil unless hash
|
103
132
|
return v1_envelope(hash) if hash.key?('x-amz-key')
|
104
133
|
return v2_envelope(hash) if hash.key?('x-amz-key-v2')
|
105
134
|
if hash.keys.any? { |key| key.match(/^x-amz-key-(.+)$/) }
|
106
135
|
msg = "unsupported envelope encryption version #{$1}"
|
107
136
|
raise Errors::DecryptionError, msg
|
108
|
-
else
|
109
|
-
nil # no envelope found
|
110
137
|
end
|
111
138
|
end
|
112
139
|
|
@@ -120,39 +147,31 @@ module Aws
|
|
120
147
|
msg = "unsupported content encrypting key (cek) format: #{alg}"
|
121
148
|
raise Errors::DecryptionError, msg
|
122
149
|
end
|
123
|
-
unless envelope['x-amz-wrap-alg']
|
124
|
-
# possible to support
|
125
|
-
# RSA/ECB/OAEPWithSHA-256AndMGF1Padding
|
150
|
+
unless POSSIBLE_WRAPPING_FORMATS.include? envelope['x-amz-wrap-alg']
|
126
151
|
alg = envelope['x-amz-wrap-alg'].inspect
|
127
152
|
msg = "unsupported key wrapping algorithm: #{alg}"
|
128
153
|
raise Errors::DecryptionError, msg
|
129
154
|
end
|
130
|
-
unless V2_ENVELOPE_KEYS
|
155
|
+
unless (missing_keys = V2_ENVELOPE_KEYS - envelope.keys).empty?
|
131
156
|
msg = "incomplete v2 encryption envelope:\n"
|
132
|
-
msg += "
|
133
|
-
msg += " got: #{envelope_keys.join(', ')}"
|
157
|
+
msg += " missing: #{missing_keys.join(',')}\n"
|
134
158
|
raise Errors::DecryptionError, msg
|
135
159
|
end
|
136
160
|
envelope
|
137
161
|
end
|
138
162
|
|
139
|
-
# When the x-amz-meta-x-amz-tag-len header is present, it indicates
|
140
|
-
# that the body of this object has a trailing auth tag. The header
|
141
|
-
# indicates the length of that tag.
|
142
|
-
#
|
143
163
|
# This method fetches the tag from the end of the object by
|
144
164
|
# making a GET Object w/range request. This auth tag is used
|
145
165
|
# to initialize the cipher, and the decrypter truncates the
|
146
166
|
# auth tag from the body when writing the final bytes.
|
147
|
-
def authenticated_decrypter(context, cipher)
|
167
|
+
def authenticated_decrypter(context, cipher, envelope)
|
148
168
|
if RUBY_VERSION.match(/1.9/)
|
149
|
-
raise "authenticated decryption not supported by
|
169
|
+
raise "authenticated decryption not supported by OpenSSL in Ruby version ~> 1.9"
|
150
170
|
raise Aws::Errors::NonSupportedRubyVersionError, msg
|
151
171
|
end
|
152
172
|
http_resp = context.http_response
|
153
173
|
content_length = http_resp.headers['content-length'].to_i
|
154
|
-
auth_tag_length =
|
155
|
-
auth_tag_length = auth_tag_length.to_i / 8
|
174
|
+
auth_tag_length = auth_tag_length(envelope)
|
156
175
|
|
157
176
|
auth_tag = context.client.get_object(
|
158
177
|
bucket: context.params[:bucket],
|
@@ -164,23 +183,39 @@ module Aws
|
|
164
183
|
cipher.auth_data = ''
|
165
184
|
|
166
185
|
# The encrypted object contains both the cipher text
|
167
|
-
# plus a trailing auth tag.
|
168
|
-
# expect for the trailing auth tag.
|
186
|
+
# plus a trailing auth tag.
|
169
187
|
IOAuthDecrypter.new(
|
170
188
|
io: http_resp.body,
|
171
189
|
encrypted_content_length: content_length - auth_tag_length,
|
172
190
|
cipher: cipher)
|
173
191
|
end
|
174
192
|
|
175
|
-
def body_contains_auth_tag?(
|
176
|
-
|
193
|
+
def body_contains_auth_tag?(envelope)
|
194
|
+
AUTH_REQUIRED_CEK_ALGS.include?(envelope['x-amz-cek-alg'])
|
195
|
+
end
|
196
|
+
|
197
|
+
# Determine the auth tag length from the algorithm
|
198
|
+
# Validate it against the value provided in the x-amz-tag-len
|
199
|
+
# Return the tag length in bytes
|
200
|
+
def auth_tag_length(envelope)
|
201
|
+
tag_length =
|
202
|
+
case envelope['x-amz-cek-alg']
|
203
|
+
when 'AES/GCM/NoPadding' then AES_GCM_TAG_LEN_BYTES
|
204
|
+
else
|
205
|
+
raise ArgumentError, 'Unsupported cek-alg: ' \
|
206
|
+
"#{envelope['x-amz-cek-alg']}"
|
207
|
+
end
|
208
|
+
if (tag_length * 8) != envelope['x-amz-tag-len'].to_i
|
209
|
+
raise Errors::DecryptionError, 'x-amz-tag-len does not match expected'
|
210
|
+
end
|
211
|
+
tag_length
|
177
212
|
end
|
178
213
|
|
179
214
|
def apply_cse_user_agent(context)
|
180
215
|
if context.config.user_agent_suffix.nil?
|
181
|
-
context.config.user_agent_suffix =
|
182
|
-
elsif !context.config.user_agent_suffix.include?
|
183
|
-
context.config.user_agent_suffix +=
|
216
|
+
context.config.user_agent_suffix = EC_USER_AGENT
|
217
|
+
elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
|
218
|
+
context.config.user_agent_suffix += " #{EC_USER_AGENT}"
|
184
219
|
end
|
185
220
|
end
|
186
221
|
|
@@ -26,11 +26,48 @@ module Aws
|
|
26
26
|
|
27
27
|
# @return [Cipher] Given an encryption envelope, returns a
|
28
28
|
# decryption cipher.
|
29
|
-
def decryption_cipher(envelope)
|
29
|
+
def decryption_cipher(envelope, options = {})
|
30
30
|
master_key = @key_provider.key_for(envelope['x-amz-matdesc'])
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
if envelope.key? 'x-amz-key'
|
32
|
+
# Support for decryption of legacy objects
|
33
|
+
key = Utils.decrypt(master_key, decode64(envelope['x-amz-key']))
|
34
|
+
iv = decode64(envelope['x-amz-iv'])
|
35
|
+
Utils.aes_decryption_cipher(:CBC, key, iv)
|
36
|
+
else
|
37
|
+
if envelope['x-amz-cek-alg'] != 'AES/GCM/NoPadding'
|
38
|
+
raise ArgumentError, 'Unsupported cek-alg: ' \
|
39
|
+
"#{envelope['x-amz-cek-alg']}"
|
40
|
+
end
|
41
|
+
key =
|
42
|
+
case envelope['x-amz-wrap-alg']
|
43
|
+
when 'AES/GCM'
|
44
|
+
if master_key.is_a? OpenSSL::PKey::RSA
|
45
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
46
|
+
' with an RSA key and the x-amz-wrap-alg is AES/GCM.'
|
47
|
+
end
|
48
|
+
Utils.decrypt_aes_gcm(master_key,
|
49
|
+
decode64(envelope['x-amz-key-v2']),
|
50
|
+
envelope['x-amz-cek-alg'])
|
51
|
+
when 'RSA-OAEP-SHA1'
|
52
|
+
unless master_key.is_a? OpenSSL::PKey::RSA
|
53
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
54
|
+
' with an AES key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
|
55
|
+
end
|
56
|
+
key, cek_alg = Utils.decrypt_rsa(master_key, decode64(envelope['x-amz-key-v2']))
|
57
|
+
raise Errors::DecryptionError unless cek_alg == envelope['x-amz-cek-alg']
|
58
|
+
key
|
59
|
+
when 'kms+context'
|
60
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
61
|
+
' with a user provided key and the x-amz-wrap-alg is' \
|
62
|
+
' kms+context. Please configure the client with the' \
|
63
|
+
' required kms_key_id'
|
64
|
+
else
|
65
|
+
raise ArgumentError, 'Unsupported wrap-alg: ' \
|
66
|
+
"#{envelope['x-amz-wrap-alg']}"
|
67
|
+
end
|
68
|
+
iv = decode64(envelope['x-amz-iv'])
|
69
|
+
Utils.aes_decryption_cipher(:GCM, key, iv)
|
70
|
+
end
|
34
71
|
end
|
35
72
|
|
36
73
|
private
|
@@ -58,7 +95,6 @@ module Aws
|
|
58
95
|
def decode64(str)
|
59
96
|
Base64.decode64(str)
|
60
97
|
end
|
61
|
-
|
62
98
|
end
|
63
99
|
end
|
64
100
|
end
|
@@ -39,8 +39,8 @@ module Aws
|
|
39
39
|
context.params[:body] = IOEncrypter.new(cipher, io)
|
40
40
|
context.params[:metadata] ||= {}
|
41
41
|
context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
|
42
|
-
if
|
43
|
-
|
42
|
+
if context.params.delete(:content_md5)
|
43
|
+
warn('Setting content_md5 on client side encrypted objects is deprecated')
|
44
44
|
end
|
45
45
|
context.http_response.on_headers do
|
46
46
|
context.params[:body].close
|
@@ -49,9 +49,9 @@ module Aws
|
|
49
49
|
|
50
50
|
def apply_cse_user_agent(context)
|
51
51
|
if context.config.user_agent_suffix.nil?
|
52
|
-
context.config.user_agent_suffix =
|
53
|
-
elsif !context.config.user_agent_suffix.include?
|
54
|
-
context.config.user_agent_suffix +=
|
52
|
+
context.config.user_agent_suffix = EC_USER_AGENT
|
53
|
+
elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
|
54
|
+
context.config.user_agent_suffix += " #{EC_USER_AGENT}"
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
@@ -9,9 +9,10 @@ module Aws
|
|
9
9
|
# @param [OpenSSL::Cipher] cipher
|
10
10
|
# @param [IO#write] io An IO-like object that responds to `#write`.
|
11
11
|
def initialize(cipher, io)
|
12
|
-
@cipher = cipher
|
12
|
+
@cipher = cipher
|
13
13
|
# Ensure that IO is reset between retries
|
14
14
|
@io = io.tap { |io| io.truncate(0) if io.respond_to?(:truncate) }
|
15
|
+
@cipher_buffer = String.new
|
15
16
|
end
|
16
17
|
|
17
18
|
# @return [#write]
|
@@ -19,17 +20,17 @@ module Aws
|
|
19
20
|
|
20
21
|
def write(chunk)
|
21
22
|
# decrypt and write
|
22
|
-
@
|
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
|
23
28
|
end
|
24
29
|
|
25
30
|
def finalize
|
26
31
|
@io.write(@cipher.final)
|
27
32
|
end
|
28
33
|
|
29
|
-
def size
|
30
|
-
@io.size
|
31
|
-
end
|
32
|
-
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
@@ -36,15 +36,36 @@ module Aws
|
|
36
36
|
|
37
37
|
# @return [Cipher] Given an encryption envelope, returns a
|
38
38
|
# decryption cipher.
|
39
|
-
def decryption_cipher(envelope)
|
39
|
+
def decryption_cipher(envelope, options = {})
|
40
40
|
encryption_context = Json.load(envelope['x-amz-matdesc'])
|
41
|
+
cek_alg = envelope['x-amz-cek-alg']
|
42
|
+
|
43
|
+
case envelope['x-amz-wrap-alg']
|
44
|
+
when 'kms'; # NO OP
|
45
|
+
when 'kms+context'
|
46
|
+
if cek_alg != encryption_context['aws:x-amz-cek-alg']
|
47
|
+
raise Errors::DecryptionError, 'Value of cek-alg from envelope'\
|
48
|
+
' does not match the value in the encryption context'
|
49
|
+
end
|
50
|
+
when 'AES/GCM'
|
51
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
52
|
+
' with a KMS key and the x-amz-wrap-alg is AES/GCM.'
|
53
|
+
when 'RSA-OAEP-SHA1'
|
54
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
55
|
+
' with a KMS key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
|
56
|
+
else
|
57
|
+
raise ArgumentError, 'Unsupported wrap-alg: ' \
|
58
|
+
"#{envelope['x-amz-wrap-alg']}"
|
59
|
+
end
|
60
|
+
|
41
61
|
key = @kms_client.decrypt(
|
42
62
|
ciphertext_blob: decode64(envelope['x-amz-key-v2']),
|
43
|
-
encryption_context: encryption_context
|
63
|
+
encryption_context: encryption_context
|
44
64
|
).plaintext
|
65
|
+
|
45
66
|
iv = decode64(envelope['x-amz-iv'])
|
46
67
|
block_mode =
|
47
|
-
case
|
68
|
+
case cek_alg
|
48
69
|
when 'AES/CBC/PKCS5Padding'
|
49
70
|
:CBC
|
50
71
|
when 'AES/CBC/PKCS7Padding'
|
@@ -61,6 +82,14 @@ module Aws
|
|
61
82
|
|
62
83
|
private
|
63
84
|
|
85
|
+
def build_encryption_context(cek_alg, options = {})
|
86
|
+
kms_context = (options[:kms_encryption_context] || {})
|
87
|
+
.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
88
|
+
{
|
89
|
+
'aws:x-amz-cek-alg' => cek_alg
|
90
|
+
}.merge(kms_context)
|
91
|
+
end
|
92
|
+
|
64
93
|
def encode64(str)
|
65
94
|
Base64.encode64(str).split("\n") * ""
|
66
95
|
end
|
@@ -39,6 +39,29 @@ module Aws
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
|
43
|
+
def decrypt_aes_gcm(key, data, auth_data)
|
44
|
+
# data is iv (12B) + key + tag (16B)
|
45
|
+
buf = data.unpack('C*')
|
46
|
+
iv = buf[0,12].pack('C*') # iv will always be 12 bytes
|
47
|
+
tag = buf[-16, 16].pack('C*') # tag is 16 bytes
|
48
|
+
enc_key = buf[12, buf.size - (12+16)].pack('C*')
|
49
|
+
cipher = aes_cipher(:decrypt, :GCM, key, iv)
|
50
|
+
cipher.auth_tag = tag
|
51
|
+
cipher.auth_data = auth_data
|
52
|
+
cipher.update(enc_key) + cipher.final
|
53
|
+
end
|
54
|
+
|
55
|
+
# returns the decrypted data + auth_data
|
56
|
+
def decrypt_rsa(key, enc_data)
|
57
|
+
# Plaintext must be KeyLengthInBytes (1 Byte) + DataKey + AuthData
|
58
|
+
buf = key.private_decrypt(enc_data, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING).unpack('C*')
|
59
|
+
key_length = buf[0]
|
60
|
+
data = buf[1, key_length].pack('C*')
|
61
|
+
auth_data = buf[key_length+1, buf.length - key_length].pack('C*')
|
62
|
+
[data, auth_data]
|
63
|
+
end
|
64
|
+
|
42
65
|
# @param [String] block_mode "CBC" or "ECB"
|
43
66
|
# @param [OpenSSL::PKey::RSA, String, nil] key
|
44
67
|
# @param [String, nil] iv The initialization vector
|
@@ -1,16 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
|
3
5
|
module Aws
|
4
6
|
module S3
|
5
7
|
|
8
|
+
REQUIRED_PARAMS = [:key_wrap_schema, :content_encryption_schema, :security_profile]
|
9
|
+
SUPPORTED_SECURITY_PROFILES = [:v2, :v2_and_legacy]
|
10
|
+
|
6
11
|
# Provides an encryption client that encrypts and decrypts data client-side,
|
7
|
-
# storing the encrypted data in Amazon S3. The EncryptionV2::Client
|
8
|
-
# provides improved security over the Encryption::Client
|
9
|
-
# modern and secure algorithms.
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
# Encryption::Client.
|
12
|
+
# storing the encrypted data in Amazon S3. The `EncryptionV2::Client` (V2 Client)
|
13
|
+
# provides improved security over the `Encryption::Client` (V1 Client)
|
14
|
+
# by using more modern and secure algorithms. You can use the V2 Client
|
15
|
+
# to continue decrypting objects encrypted using deprecated algorithms
|
16
|
+
# by setting security_profile: :v2_and_legacy. The latest V1 Client also
|
17
|
+
# supports reading and decrypting objects encrypted by the V2 Client.
|
14
18
|
#
|
15
19
|
# This client uses a process called "envelope encryption". Your private
|
16
20
|
# encryption keys and your data's plain-text are **never** sent to
|
@@ -47,7 +51,12 @@ module Aws
|
|
47
51
|
# key = OpenSSL::PKey::RSA.new(1024)
|
48
52
|
#
|
49
53
|
# # encryption client
|
50
|
-
# s3 = Aws::S3::EncryptionV2::Client.new(
|
54
|
+
# s3 = Aws::S3::EncryptionV2::Client.new(
|
55
|
+
# encryption_key: key,
|
56
|
+
# key_wrap_schema: :rsa_oaep_sha1, # the key_wrap_schema must be rsa_oaep_sha1 for asymmetric keys
|
57
|
+
# content_encryption_schema: :aes_gcm_no_padding,
|
58
|
+
# security_profile: :v2 # use :v2_and_legacy to allow reading/decrypting objects encrypted by the V1 encryption client
|
59
|
+
# )
|
51
60
|
#
|
52
61
|
# # round-trip an object, encrypted/decrypted locally
|
53
62
|
# s3.put_object(bucket:'aws-sdk', key:'secret', body:'handshake')
|
@@ -59,6 +68,20 @@ module Aws
|
|
59
68
|
# Aws::S3::Client.new.get_object(bucket:'aws-sdk', key:'secret').body.read
|
60
69
|
# #=> "... cipher text ..."
|
61
70
|
#
|
71
|
+
# ## Required Configuration
|
72
|
+
#
|
73
|
+
# You must configure all of the following:
|
74
|
+
#
|
75
|
+
# * a key or key provider - See the Keys section below. The key provided determines
|
76
|
+
# the key wrapping schema(s) supported for both encryption and decryption.
|
77
|
+
# * `key_wrap_schema` - The key wrapping schema. It must match the type of key configured.
|
78
|
+
# * `content_encryption_schema` - The only supported value currently is `:aes_gcm_no_padding`.
|
79
|
+
# More options will be added in future releases.
|
80
|
+
# * `security_profile` - Determines the support for reading objects written
|
81
|
+
# using older key wrap or content encryption schemas. If you need to read
|
82
|
+
# legacy objects encrypted by an existing V1 Client, then set this to `:v2_and_legacy`.
|
83
|
+
# Otherwise, set it to `:v2`
|
84
|
+
#
|
62
85
|
# ## Keys
|
63
86
|
#
|
64
87
|
# For client-side encryption to work, you must provide one of the following:
|
@@ -67,15 +90,25 @@ module Aws
|
|
67
90
|
# * A {KeyProvider}
|
68
91
|
# * A KMS encryption key id
|
69
92
|
#
|
93
|
+
# Additionally, the key wrapping schema must agree with the type of the key:
|
94
|
+
# * :aes_gcm: An AES encryption key or a key provider.
|
95
|
+
# * :rsa_oaep_sha1: An RSA encryption key or key provider.
|
96
|
+
# * :kms_context: A KMS encryption key id
|
97
|
+
#
|
70
98
|
# ### An Encryption Key
|
71
99
|
#
|
72
100
|
# You can pass a single encryption key. This is used as a master key
|
73
101
|
# encrypting and decrypting all object keys.
|
74
102
|
#
|
75
|
-
# key = OpenSSL::Cipher.new("AES-256-ECB").random_key # symmetric key
|
76
|
-
# key = OpenSSL::PKey::RSA.new(1024) # asymmetric key pair
|
103
|
+
# key = OpenSSL::Cipher.new("AES-256-ECB").random_key # symmetric key - used with `key_wrap_schema: :aes_gcm`
|
104
|
+
# key = OpenSSL::PKey::RSA.new(1024) # asymmetric key pair - used with `key_wrap_schema: :rsa_oaep_sha1`
|
77
105
|
#
|
78
|
-
# s3 = Aws::S3::EncryptionV2::Client.new(
|
106
|
+
# s3 = Aws::S3::EncryptionV2::Client.new(
|
107
|
+
# encryption_key: key,
|
108
|
+
# key_wrap_schema: :aes_gcm, # or :rsa_oaep_sha1 if using RSA
|
109
|
+
# content_encryption_schema: :aes_gcm_no_padding,
|
110
|
+
# security_profile: :v2
|
111
|
+
# )
|
79
112
|
#
|
80
113
|
# ### Key Provider
|
81
114
|
#
|
@@ -84,8 +117,9 @@ module Aws
|
|
84
117
|
#
|
85
118
|
# ### KMS Encryption Key Id
|
86
119
|
#
|
87
|
-
# If you pass the id
|
88
|
-
# then KMS will be used to
|
120
|
+
# If you pass the id of an AWS Key Management Service (KMS) key and
|
121
|
+
# use :kms_content for the key_wrap_schema, then KMS will be used to
|
122
|
+
# generate, encrypt and decrypt object keys.
|
89
123
|
#
|
90
124
|
# # keep track of the kms key id
|
91
125
|
# kms = Aws::KMS::Client.new
|
@@ -94,6 +128,9 @@ module Aws
|
|
94
128
|
# Aws::S3::EncryptionV2::Client.new(
|
95
129
|
# kms_key_id: key_id,
|
96
130
|
# kms_client: kms,
|
131
|
+
# key_wrap_schema: :kms_context,
|
132
|
+
# content_encryption_schema: :aes_gcm_no_padding,
|
133
|
+
# security_profile: :v2
|
97
134
|
# )
|
98
135
|
#
|
99
136
|
# ## Custom Key Providers
|
@@ -142,7 +179,12 @@ module Aws
|
|
142
179
|
#
|
143
180
|
# # chooses the key based on the materials description stored
|
144
181
|
# # with the encrypted object
|
145
|
-
# s3 = Aws::S3::EncryptionV2::Client.new(
|
182
|
+
# s3 = Aws::S3::EncryptionV2::Client.new(
|
183
|
+
# key_provider: keys,
|
184
|
+
# key_wrap_schema: ...,
|
185
|
+
# content_encryption_schema: :aes_gcm_no_padding,
|
186
|
+
# security_profile: :v2
|
187
|
+
# )
|
146
188
|
#
|
147
189
|
# ## Materials Description
|
148
190
|
#
|
@@ -176,6 +218,9 @@ module Aws
|
|
176
218
|
# key_provider: ...,
|
177
219
|
# envelope_location: :instruction_file,
|
178
220
|
# instruction_file_suffix: '.instruction' # default
|
221
|
+
# key_wrap_schema: ...,
|
222
|
+
# content_encryption_schema: :aes_gcm_no_padding,
|
223
|
+
# security_profile: :v2
|
179
224
|
# )
|
180
225
|
#
|
181
226
|
# When using an instruction file, multiple requests are made when
|
@@ -189,8 +234,19 @@ module Aws
|
|
189
234
|
extend Forwardable
|
190
235
|
def_delegators :@client, :config, :delete_object, :head_object, :build_request
|
191
236
|
|
192
|
-
# Creates a new encryption client. You must
|
193
|
-
#
|
237
|
+
# Creates a new encryption client. You must configure all of the following:
|
238
|
+
#
|
239
|
+
# * a key or key provider - The key provided also determines the key wrapping
|
240
|
+
# schema(s) supported for both encryption and decryption.
|
241
|
+
# * `key_wrap_schema` - The key wrapping schema. It must match the type of key configured.
|
242
|
+
# * `content_encryption_schema` - The only supported value currently is `:aes_gcm_no_padding`
|
243
|
+
# More options will be added in future releases.
|
244
|
+
# * `security_profile` - Determines the support for reading objects written
|
245
|
+
# using older key wrap or content encryption schemas. If you need to read
|
246
|
+
# legacy objects encrypted by an existing V1 Client, then set this to `:v2_and_legacy`.
|
247
|
+
# Otherwise, set it to `:v2`
|
248
|
+
#
|
249
|
+
# To configure the key you must provide one of the following set of options:
|
194
250
|
#
|
195
251
|
# * `:encryption_key`
|
196
252
|
# * `:kms_key_id`
|
@@ -209,12 +265,36 @@ module Aws
|
|
209
265
|
# then AWS Key Management Service (KMS) will be used to manage the
|
210
266
|
# object encryption keys. By default a {KMS::Client} will be
|
211
267
|
# constructed for KMS API calls. Alternatively, you can provide
|
212
|
-
# your own via `:kms_client`.
|
268
|
+
# your own via `:kms_client`. To only support decryption/reads, you may
|
269
|
+
# provide `:allow_decrypt_with_any_cmk` which will use
|
270
|
+
# the implicit CMK associated with the data during reads but will
|
271
|
+
# not allow you to encrypt/write objects with this client.
|
213
272
|
#
|
214
273
|
# @option options [#key_for] :key_provider Any object that responds
|
215
274
|
# to `#key_for`. This method should accept a materials description
|
216
275
|
# JSON document string and return return an encryption key.
|
217
276
|
#
|
277
|
+
# @option options [required, Symbol] :key_wrap_schema The Key wrapping
|
278
|
+
# schema to be used. It must match the type of key configured.
|
279
|
+
# Must be one of the following:
|
280
|
+
#
|
281
|
+
# * :kms_context (Must provide kms_key_id)
|
282
|
+
# * :aes_gcm (Must provide an AES (string) key)
|
283
|
+
# * :rsa_oaep_sha1 (Must provide an RSA key)
|
284
|
+
#
|
285
|
+
# @option options [required, Symbol] :content_encryption_schema
|
286
|
+
# Must be one of the following:
|
287
|
+
#
|
288
|
+
# * :aes_gcm_no_padding
|
289
|
+
#
|
290
|
+
# @option options [Required, Symbol] :security_profile
|
291
|
+
# Determines the support for reading objects written using older
|
292
|
+
# key wrap or content encryption schemas.
|
293
|
+
# Must be one of the following:
|
294
|
+
#
|
295
|
+
# * :v2 - Reads of legacy (v1) objects are NOT allowed
|
296
|
+
# * :v2_and_legacy - Enables reading of legacy (V1) schemas.
|
297
|
+
#
|
218
298
|
# @option options [Symbol] :envelope_location (:metadata) Where to
|
219
299
|
# store the envelope encryption keys. By default, the envelope is
|
220
300
|
# stored with the encrypted object. If you pass `:instruction_file`,
|
@@ -228,10 +308,14 @@ module Aws
|
|
228
308
|
# is constructed when using KMS to manage encryption keys.
|
229
309
|
#
|
230
310
|
def initialize(options = {})
|
311
|
+
validate_params(options)
|
231
312
|
@client = extract_client(options)
|
232
313
|
@cipher_provider = cipher_provider(options)
|
233
314
|
@envelope_location = extract_location(options)
|
234
315
|
@instruction_file_suffix = extract_suffix(options)
|
316
|
+
@kms_allow_decrypt_with_any_cmk =
|
317
|
+
options[:kms_key_id] == :kms_allow_decrypt_with_any_cmk
|
318
|
+
@security_profile = extract_security_profile(options)
|
235
319
|
end
|
236
320
|
|
237
321
|
# @return [S3::Client]
|
@@ -241,6 +325,14 @@ module Aws
|
|
241
325
|
# AWS Key Management Service (KMS).
|
242
326
|
attr_reader :key_provider
|
243
327
|
|
328
|
+
# @return [Symbol] Determines the support for reading objects written
|
329
|
+
# using older key wrap or content encryption schemas.
|
330
|
+
attr_reader :security_profile
|
331
|
+
|
332
|
+
# @return [Boolean] If true the provided KMS key_id will not be used
|
333
|
+
# during decrypt, allowing decryption with the key_id from the object.
|
334
|
+
attr_reader :kms_allow_decrypt_with_any_cmk
|
335
|
+
|
244
336
|
# @return [Symbol<:metadata, :instruction_file>]
|
245
337
|
attr_reader :envelope_location
|
246
338
|
|
@@ -272,9 +364,22 @@ module Aws
|
|
272
364
|
req.send_request
|
273
365
|
end
|
274
366
|
|
275
|
-
# Gets an object from Amazon S3, decrypting
|
367
|
+
# Gets an object from Amazon S3, decrypting data locally.
|
276
368
|
# See {S3::Client#get_object} for documentation on accepted
|
277
369
|
# request parameters.
|
370
|
+
# Warning: If you provide a block to get_object or set the request
|
371
|
+
# parameter :response_target to a Proc, then read the entire object to the
|
372
|
+
# end before you start using the decrypted data. This is to verify that
|
373
|
+
# the object has not been modified since it was encrypted.
|
374
|
+
#
|
375
|
+
# @option options [Symbol] :security_profile
|
376
|
+
# Determines the support for reading objects written using older
|
377
|
+
# key wrap or content encryption schemas. Overrides the value set
|
378
|
+
# on client construction if provided.
|
379
|
+
# Must be one of the following:
|
380
|
+
#
|
381
|
+
# * :v2 - Reads of legacy (v1) objects are NOT allowed
|
382
|
+
# * :v2_and_legacy - Enables reading of legacy (V1) schemas.
|
278
383
|
# @option params [String] :instruction_file_suffix The suffix
|
279
384
|
# used to find the instruction file containing the encryption
|
280
385
|
# envelope. You should not set this option when the envelope
|
@@ -282,30 +387,58 @@ module Aws
|
|
282
387
|
# {#instruction_file_suffix}.
|
283
388
|
# @option params [Hash] :kms_encryption_context Additional encryption
|
284
389
|
# context to use with KMS. Applies only when KMS is used.
|
285
|
-
# @option
|
390
|
+
# @option options [Boolean] :kms_allow_decrypt_with_any_cmk (false)
|
391
|
+
# By default the KMS CMK ID (kms_key_id) will be used during decrypt
|
392
|
+
# and will fail if there is a mismatch. Setting this to true
|
393
|
+
# will use the implicit CMK associated with the data.
|
286
394
|
# @option (see S3::Client#get_object)
|
287
395
|
# @return (see S3::Client#get_object)
|
288
396
|
# @see S3::Client#get_object
|
289
|
-
# @note The `:range` request parameter is not
|
397
|
+
# @note The `:range` request parameter is not supported.
|
290
398
|
def get_object(params = {}, &block)
|
291
399
|
if params[:range]
|
292
400
|
raise NotImplementedError, '#get_object with :range not supported'
|
293
401
|
end
|
294
402
|
envelope_location, instruction_file_suffix = envelope_options(params)
|
295
403
|
kms_encryption_context = params.delete(:kms_encryption_context)
|
404
|
+
kms_any_cmk_mode = kms_any_cmk_mode(params)
|
405
|
+
security_profile = security_profile_from_params(params)
|
406
|
+
|
296
407
|
req = @client.build_request(:get_object, params)
|
297
408
|
req.handlers.add(DecryptHandler)
|
298
409
|
req.context[:encryption] = {
|
299
410
|
cipher_provider: @cipher_provider,
|
300
411
|
envelope_location: envelope_location,
|
301
412
|
instruction_file_suffix: instruction_file_suffix,
|
302
|
-
kms_encryption_context: kms_encryption_context
|
413
|
+
kms_encryption_context: kms_encryption_context,
|
414
|
+
kms_allow_decrypt_with_any_cmk: kms_any_cmk_mode,
|
415
|
+
security_profile: security_profile
|
303
416
|
}
|
304
417
|
req.send_request(target: block)
|
305
418
|
end
|
306
419
|
|
307
420
|
private
|
308
421
|
|
422
|
+
# Validate required parameters exist and don't conflict.
|
423
|
+
# The cek_alg and wrap_alg are passed on to the CipherProviders
|
424
|
+
# and further validated there
|
425
|
+
def validate_params(options)
|
426
|
+
unless (missing_params = REQUIRED_PARAMS - options.keys).empty?
|
427
|
+
raise ArgumentError, "Missing required parameter(s): "\
|
428
|
+
"#{missing_params.map{ |s| ":#{s}" }.join(', ')}"
|
429
|
+
end
|
430
|
+
|
431
|
+
wrap_alg = options[:key_wrap_schema]
|
432
|
+
|
433
|
+
# validate that the wrap alg matches the type of key given
|
434
|
+
case wrap_alg
|
435
|
+
when :kms_context
|
436
|
+
unless options[:kms_key_id]
|
437
|
+
raise ArgumentError, 'You must provide :kms_key_id to use :kms_context'
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
309
442
|
def extract_client(options)
|
310
443
|
options[:client] || begin
|
311
444
|
options = options.dup
|
@@ -315,6 +448,7 @@ module Aws
|
|
315
448
|
options.delete(:encryption_key)
|
316
449
|
options.delete(:envelope_location)
|
317
450
|
options.delete(:instruction_file_suffix)
|
451
|
+
REQUIRED_PARAMS.each { |p| options.delete(p) }
|
318
452
|
S3::Client.new(options)
|
319
453
|
end
|
320
454
|
end
|
@@ -324,7 +458,7 @@ module Aws
|
|
324
458
|
KMS::Client.new(
|
325
459
|
region: @client.config.region,
|
326
460
|
credentials: @client.config.credentials,
|
327
|
-
|
461
|
+
)
|
328
462
|
end
|
329
463
|
end
|
330
464
|
|
@@ -333,10 +467,16 @@ module Aws
|
|
333
467
|
KmsCipherProvider.new(
|
334
468
|
kms_key_id: options[:kms_key_id],
|
335
469
|
kms_client: kms_client(options),
|
470
|
+
key_wrap_schema: options[:key_wrap_schema],
|
471
|
+
content_encryption_schema: options[:content_encryption_schema]
|
336
472
|
)
|
337
473
|
else
|
338
474
|
@key_provider = extract_key_provider(options)
|
339
|
-
DefaultCipherProvider.new(
|
475
|
+
DefaultCipherProvider.new(
|
476
|
+
key_provider: @key_provider,
|
477
|
+
key_wrap_schema: options[:key_wrap_schema],
|
478
|
+
content_encryption_schema: options[:content_encryption_schema]
|
479
|
+
)
|
340
480
|
end
|
341
481
|
end
|
342
482
|
|
@@ -382,6 +522,44 @@ module Aws
|
|
382
522
|
end
|
383
523
|
end
|
384
524
|
|
525
|
+
def kms_any_cmk_mode(params)
|
526
|
+
if !params[:kms_allow_decrypt_with_any_cmk].nil?
|
527
|
+
params.delete(:kms_allow_decrypt_with_any_cmk)
|
528
|
+
else
|
529
|
+
@kms_allow_decrypt_with_any_cmk
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
def extract_security_profile(options)
|
534
|
+
validate_security_profile(options[:security_profile])
|
535
|
+
end
|
536
|
+
|
537
|
+
def security_profile_from_params(params)
|
538
|
+
security_profile =
|
539
|
+
if !params[:security_profile].nil?
|
540
|
+
params.delete(:security_profile)
|
541
|
+
else
|
542
|
+
@security_profile
|
543
|
+
end
|
544
|
+
validate_security_profile(security_profile)
|
545
|
+
end
|
546
|
+
|
547
|
+
def validate_security_profile(security_profile)
|
548
|
+
unless SUPPORTED_SECURITY_PROFILES.include? security_profile
|
549
|
+
raise ArgumentError, "Unsupported security profile: :#{security_profile}. " \
|
550
|
+
"Please provide one of: #{SUPPORTED_SECURITY_PROFILES.map { |s| ":#{s}" }.join(', ')}"
|
551
|
+
end
|
552
|
+
if security_profile == :v2_and_legacy && !@warned_about_legacy
|
553
|
+
@warned_about_legacy = true
|
554
|
+
warn(
|
555
|
+
'The S3 Encryption Client is configured to read encrypted objects ' \
|
556
|
+
"with legacy encryption modes. If you don't have objects " \
|
557
|
+
'encrypted with these legacy modes, you should disable support ' \
|
558
|
+
'for them to enhance security.'
|
559
|
+
)
|
560
|
+
end
|
561
|
+
security_profile
|
562
|
+
end
|
385
563
|
end
|
386
564
|
end
|
387
565
|
end
|