aws-sdk-s3 1.207.0 → 1.208.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/CHANGELOG.md +5 -0
- data/VERSION +1 -1
- data/lib/aws-sdk-s3/client.rb +1 -1
- data/lib/aws-sdk-s3/customizations.rb +1 -0
- data/lib/aws-sdk-s3/encryption/client.rb +2 -2
- data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +2 -0
- data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +2 -0
- data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/client.rb +98 -23
- data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +7 -162
- data/lib/aws-sdk-s3/encryptionV2/decryption.rb +205 -0
- data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +17 -0
- data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +2 -0
- data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +8 -0
- data/lib/aws-sdk-s3/encryptionV2/utils.rb +5 -0
- data/lib/aws-sdk-s3/encryptionV3/client.rb +885 -0
- data/lib/aws-sdk-s3/encryptionV3/decrypt_handler.rb +98 -0
- data/lib/aws-sdk-s3/encryptionV3/decryption.rb +244 -0
- data/lib/aws-sdk-s3/encryptionV3/default_cipher_provider.rb +159 -0
- data/lib/aws-sdk-s3/encryptionV3/default_key_provider.rb +35 -0
- data/lib/aws-sdk-s3/encryptionV3/encrypt_handler.rb +98 -0
- data/lib/aws-sdk-s3/encryptionV3/errors.rb +47 -0
- data/lib/aws-sdk-s3/encryptionV3/io_auth_decrypter.rb +60 -0
- data/lib/aws-sdk-s3/encryptionV3/io_decrypter.rb +35 -0
- data/lib/aws-sdk-s3/encryptionV3/io_encrypter.rb +84 -0
- data/lib/aws-sdk-s3/encryptionV3/key_provider.rb +28 -0
- data/lib/aws-sdk-s3/encryptionV3/kms_cipher_provider.rb +159 -0
- data/lib/aws-sdk-s3/encryptionV3/materials.rb +58 -0
- data/lib/aws-sdk-s3/encryptionV3/utils.rb +321 -0
- data/lib/aws-sdk-s3/encryption_v2.rb +1 -0
- data/lib/aws-sdk-s3/encryption_v3.rb +24 -0
- data/lib/aws-sdk-s3.rb +1 -1
- metadata +17 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
require 'logger'
|
|
6
|
+
|
|
7
|
+
module Aws
|
|
8
|
+
module S3
|
|
9
|
+
module EncryptionV3
|
|
10
|
+
# @api private
|
|
11
|
+
class DecryptHandler < Seahorse::Client::Handler
|
|
12
|
+
@@warned_response_target_proc = false
|
|
13
|
+
|
|
14
|
+
def call(context)
|
|
15
|
+
attach_http_event_listeners(context)
|
|
16
|
+
apply_cse_user_agent(context)
|
|
17
|
+
|
|
18
|
+
if context[:response_target].is_a?(Proc) && !@@warned_response_target_proc
|
|
19
|
+
@@warned_response_target_proc = true
|
|
20
|
+
warn(':response_target is a Proc, or a block was provided. ' \
|
|
21
|
+
'Read the entire object to the ' \
|
|
22
|
+
'end before you start using the decrypted data. This is to ' \
|
|
23
|
+
'verify that the object has not been modified since it ' \
|
|
24
|
+
'was encrypted.')
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@handler.call(context)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def attach_http_event_listeners(context)
|
|
34
|
+
context.http_response.on_headers(200) do
|
|
35
|
+
##= ../specification/s3-encryption/decryption.md#key-commitment
|
|
36
|
+
##% The S3EC MUST validate the algorithm suite used for decryption
|
|
37
|
+
##% against the key commitment policy before attempting to decrypt the content ciphertext.
|
|
38
|
+
# This is because the commitment policy _always_ allows decrypting committing algorithms.
|
|
39
|
+
# In the else branch we check to see if
|
|
40
|
+
decrypter =
|
|
41
|
+
if Aws::S3::EncryptionV3::Decryption.v3?(context)
|
|
42
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status
|
|
43
|
+
##% - If the metadata contains "x-amz-3" and "x-amz-d" and "x-amz-i" then the object MUST be considered an S3EC-encrypted object using the V3 format.
|
|
44
|
+
cipher, envelope = Aws::S3::EncryptionV3::Decryption.decryption_cipher(context)
|
|
45
|
+
Aws::S3::EncryptionV3::Decryption.get_decrypter(context, cipher, envelope)
|
|
46
|
+
else
|
|
47
|
+
if context[:encryption][:commitment_policy] == :require_encrypt_require_decrypt
|
|
48
|
+
##= ../specification/s3-encryption/decryption.md#key-commitment
|
|
49
|
+
##% If the commitment policy requires decryption using a committing algorithm suite,
|
|
50
|
+
##% and the algorithm suite associated with the object does not support key commitment, then the S3EC MUST throw an exception.
|
|
51
|
+
##= ../specification/s3-encryption/key-commitment.md#commitment-policy
|
|
52
|
+
##% When the commitment policy is REQUIRE_ENCRYPT_REQUIRE_DECRYPT, the S3EC MUST NOT allow decryption using algorithm suites which do not support key commitment.
|
|
53
|
+
raise Errors::NonCommittingDecryptionError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##= ../specification/s3-encryption/key-commitment.md#commitment-policy
|
|
57
|
+
##% When the commitment policy is FORBID_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
|
|
58
|
+
##= ../specification/s3-encryption/key-commitment.md#commitment-policy
|
|
59
|
+
##% When the commitment policy is REQUIRE_ENCRYPT_ALLOW_DECRYPT, the S3EC MUST allow decryption using algorithm suites which do not support key commitment.
|
|
60
|
+
cipher, envelope = Aws::S3::EncryptionV2::Decryption.decryption_cipher(context)
|
|
61
|
+
Aws::S3::EncryptionV2::Decryption.get_decrypter(context, cipher, envelope)
|
|
62
|
+
end
|
|
63
|
+
context.http_response.body = decrypter
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context.http_response.on_success(200) do
|
|
67
|
+
decrypter = context.http_response.body
|
|
68
|
+
decrypter.finalize
|
|
69
|
+
decrypter.io.rewind if decrypter.io.respond_to?(:rewind)
|
|
70
|
+
context.http_response.body = decrypter.io
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context.http_response.on_error do
|
|
74
|
+
context.http_response.body = context.http_response.body.io if context.http_response.body.respond_to?(:io)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def apply_cse_user_agent(context)
|
|
79
|
+
if context.config.user_agent_suffix.nil?
|
|
80
|
+
context.config.user_agent_suffix = EC_USER_AGENT
|
|
81
|
+
elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
|
|
82
|
+
context.config.user_agent_suffix += " #{EC_USER_AGENT}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
|
|
91
|
+
##= type=exception
|
|
92
|
+
##= reason=This has never been supported in Ruby
|
|
93
|
+
##% This material description string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server.
|
|
94
|
+
|
|
95
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
|
|
96
|
+
##= type=exception
|
|
97
|
+
##= reason=This has never been supported in Ruby
|
|
98
|
+
##% This encryption context string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module Aws
|
|
6
|
+
module S3
|
|
7
|
+
module EncryptionV3
|
|
8
|
+
# @api private
|
|
9
|
+
class Decryption
|
|
10
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
11
|
+
##= type=implication
|
|
12
|
+
##% The "x-amz-" prefix denotes that the metadata is owned by an Amazon product and MUST be prepended to all S3EC metadata mapkeys.
|
|
13
|
+
|
|
14
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
15
|
+
##% - The mapkey "x-amz-3" MUST be present for V3 format objects.
|
|
16
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
17
|
+
##% - The mapkey "x-amz-w" MUST be present for V3 format objects.
|
|
18
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
19
|
+
##= type=implication
|
|
20
|
+
##% - This mapkey ("x-amz-3") SHOULD be represented by a constant named "ENCRYPTED_DATA_KEY_V3" or similar in the implementation code.
|
|
21
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
22
|
+
##= type=implication
|
|
23
|
+
##% - This mapkey ("x-amz-w") SHOULD be represented by a constant named "ENCRYPTED_DATA_KEY_ALGORITHM_V3" or similar in the implementation code.
|
|
24
|
+
ENVELOP_KEY = %w[
|
|
25
|
+
x-amz-3
|
|
26
|
+
x-amz-w
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
30
|
+
##% - The mapkey "x-amz-m" SHOULD be present for V3 format objects that use Raw Keyring Material Description.
|
|
31
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
32
|
+
##% - The mapkey "x-amz-t" SHOULD be present for V3 format objects that use KMS Encryption Context.
|
|
33
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
34
|
+
##= type=implication
|
|
35
|
+
##% - This mapkey ("x-amz-m") SHOULD be represented by a constant named "MAT_DESC_V3" or similar in the implementation code.
|
|
36
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
37
|
+
##= type=implication
|
|
38
|
+
##% - This mapkey ("x-amz-t") SHOULD be represented by a constant named "ENCRYPTION_CONTEXT_V3" or similar in the implementation code.
|
|
39
|
+
OPTIONAL_ENVELOP_KEY = %w[
|
|
40
|
+
x-amz-m
|
|
41
|
+
x-amz-t
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
45
|
+
##% - The mapkey "x-amz-c" MUST be present for V3 format objects.
|
|
46
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
47
|
+
##% - The mapkey "x-amz-d" MUST be present for V3 format objects.
|
|
48
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
49
|
+
##% - The mapkey "x-amz-i" MUST be present for V3 format objects.
|
|
50
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
51
|
+
##= type=implication
|
|
52
|
+
##% - This mapkey ("x-amz-c") SHOULD be represented by a constant named "CONTENT_CIPHER_V3" or similar in the implementation code.
|
|
53
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
54
|
+
##= type=implication
|
|
55
|
+
##% - This mapkey ("x-amz-d") SHOULD be represented by a constant named "KEY_COMMITMENT_V3" or similar in the implementation code.
|
|
56
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
57
|
+
##= type=implication
|
|
58
|
+
##% - This mapkey ("x-amz-i") SHOULD be represented by a constant named "MESSAGE_ID_V3" or similar in the implementation code.
|
|
59
|
+
METADATA_KEY = %w[
|
|
60
|
+
x-amz-c
|
|
61
|
+
x-amz-d
|
|
62
|
+
x-amz-i
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
# Reference V2's envelope keys rather than duplicating them
|
|
66
|
+
LEGACY_POSSIBLE_ENVELOPE_KEYS = Aws::S3::EncryptionV2::Decryption::POSSIBLE_ENVELOPE_KEYS
|
|
67
|
+
|
|
68
|
+
POSSIBLE_ENVELOPE_KEYS = (ENVELOP_KEY + METADATA_KEY + OPTIONAL_ENVELOP_KEY + LEGACY_POSSIBLE_ENVELOPE_KEYS).uniq
|
|
69
|
+
REQUIRED_ENVELOPE_KEYS = (ENVELOP_KEY + METADATA_KEY).uniq
|
|
70
|
+
|
|
71
|
+
POSSIBLE_WRAPPING_FORMATS = %w[
|
|
72
|
+
01
|
|
73
|
+
02
|
|
74
|
+
11
|
|
75
|
+
12
|
|
76
|
+
21
|
|
77
|
+
22
|
|
78
|
+
].freeze
|
|
79
|
+
|
|
80
|
+
POSSIBLE_ENCRYPTION_FORMATS = %w[
|
|
81
|
+
115
|
|
82
|
+
].freeze
|
|
83
|
+
|
|
84
|
+
class << self
|
|
85
|
+
def v3?(context)
|
|
86
|
+
context.http_response.headers.key?('x-amz-meta-x-amz-i')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def decryption_cipher(context)
|
|
90
|
+
if (envelope = get_encryption_envelope(context))
|
|
91
|
+
cipher = context[:encryption][:v3_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
|
+
# This method fetches the tag from the end of the object by
|
|
103
|
+
# making a GET Object w/range request. This auth tag is used
|
|
104
|
+
# to initialize the cipher, and the decrypter truncates the
|
|
105
|
+
# auth tag from the body when writing the final bytes.
|
|
106
|
+
def get_decrypter(context, cipher, _envelope)
|
|
107
|
+
http_resp = context.http_response
|
|
108
|
+
content_length = http_resp.headers['content-length'].to_i
|
|
109
|
+
|
|
110
|
+
# The encrypted object contains both the cipher text
|
|
111
|
+
# plus a trailing auth tag.
|
|
112
|
+
# The trailing auth tag will be accumulated and added to the cipher.auth_tag.
|
|
113
|
+
IOAuthDecrypter.new(
|
|
114
|
+
io: http_resp.body,
|
|
115
|
+
encrypted_content_length: content_length - AES_GCM_TAG_LEN_BYTES,
|
|
116
|
+
cipher: cipher
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def get_encryption_envelope(context)
|
|
121
|
+
# Get initial envelope data from :envelope_location
|
|
122
|
+
envelope =
|
|
123
|
+
if context[:encryption][:envelope_location] == :metadata
|
|
124
|
+
envelope_from_metadata(context)
|
|
125
|
+
else
|
|
126
|
+
envelope_from_instr_file(context)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# If empty or incomplete, get/merge data from secondary source
|
|
130
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status
|
|
131
|
+
##% If the object matches none of the V1/V2/V3 formats, the S3EC MUST attempt to get the instruction file.
|
|
132
|
+
if envelope.nil? || envelope.empty? || !complete_envelop?(envelope)
|
|
133
|
+
secondary =
|
|
134
|
+
if context[:encryption][:envelope_location] == :metadata
|
|
135
|
+
envelope_from_instr_file(context)
|
|
136
|
+
else
|
|
137
|
+
envelope_from_metadata(context)
|
|
138
|
+
end
|
|
139
|
+
# If we attempted to read a non-existent instruction file,
|
|
140
|
+
# then envelope would be nil,
|
|
141
|
+
# but we may find the information we need in the metadata.
|
|
142
|
+
if envelope && secondary
|
|
143
|
+
envelope.merge!(secondary)
|
|
144
|
+
elsif secondary
|
|
145
|
+
envelope = secondary
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
##= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata
|
|
150
|
+
##% If the S3EC does not support decoding the S3 Server's "double encoding" then it MUST return the content metadata untouched.
|
|
151
|
+
v3_envelope?(envelope)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def complete_envelop?(possible_envelope)
|
|
155
|
+
# V3 envelops always store some information in metadata
|
|
156
|
+
# If we look at the metadata, we may still need to check the instruction file
|
|
157
|
+
# Similarly, if we start checking the instruction file,
|
|
158
|
+
# we sill need to get the message id and commitment key from the metadata
|
|
159
|
+
envelop_count = ENVELOP_KEY.count { |key| possible_envelope.key?(key) }
|
|
160
|
+
metadata_count = METADATA_KEY.count { |key| possible_envelope.key?(key) }
|
|
161
|
+
|
|
162
|
+
# If we have all keys, we are done
|
|
163
|
+
(envelop_count == ENVELOP_KEY.size && metadata_count == METADATA_KEY.size) ||
|
|
164
|
+
# If we have 0 keys, then this is done too.
|
|
165
|
+
# Because it means we are not a v3 committing message.
|
|
166
|
+
(envelop_count.zero? && metadata_count.zero?)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def envelope_from_metadata(context)
|
|
170
|
+
POSSIBLE_ENVELOPE_KEYS.filter_map do |suffix|
|
|
171
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
172
|
+
##= type=exception
|
|
173
|
+
##= reason=Ruby is reading the headers directly
|
|
174
|
+
##% The "x-amz-meta-" prefix is automatically added by the S3 server and MUST NOT be included in implementation code.
|
|
175
|
+
if (value = context.http_response.headers["x-amz-meta-#{suffix}"])
|
|
176
|
+
##= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata
|
|
177
|
+
##= type=exception
|
|
178
|
+
##= reason=This has never been supported in Ruby
|
|
179
|
+
##% The S3EC SHOULD support decoding the S3 Server's "double encoding".
|
|
180
|
+
|
|
181
|
+
##= ../specification/s3-encryption/data-format/metadata-strategy.md#object-metadata
|
|
182
|
+
##% If the S3EC does not support decoding the S3 Server's "double encoding" then it MUST return the content metadata untouched.
|
|
183
|
+
[suffix, value]
|
|
184
|
+
end
|
|
185
|
+
end.to_h
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def envelope_from_instr_file(context)
|
|
189
|
+
suffix = context[:encryption][:instruction_file_suffix]
|
|
190
|
+
possible_envelope = Json.load(context.client.get_object(
|
|
191
|
+
bucket: context.params[:bucket],
|
|
192
|
+
key: context.params[:key] + suffix
|
|
193
|
+
).body.read)
|
|
194
|
+
unless (keys = possible_envelope.keys & METADATA_KEY).empty?
|
|
195
|
+
msg = "unsupported metadata key found in instruction file: #{keys.join(', ')}"
|
|
196
|
+
raise Errors::DecryptionError, msg
|
|
197
|
+
end
|
|
198
|
+
possible_envelope
|
|
199
|
+
rescue S3::Errors::ServiceError, Json::ParseError
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def v3_envelope?(possible_envelope)
|
|
204
|
+
unless (keys = possible_envelope.keys & LEGACY_POSSIBLE_ENVELOPE_KEYS).empty?
|
|
205
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status
|
|
206
|
+
##% If there are multiple mapkeys which are meant to be exclusive, such as "x-amz-key", "x-amz-key-v2", and "x-amz-3" then the S3EC SHOULD throw an exception.
|
|
207
|
+
msg = "legacy metadata key found: #{keys.join(', ')}"
|
|
208
|
+
raise Errors::DecryptionError, msg
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
unless POSSIBLE_ENCRYPTION_FORMATS.include? possible_envelope['x-amz-c']
|
|
212
|
+
alg = possible_envelope['x-amz-c'].inspect
|
|
213
|
+
msg = "unsupported content encrypting key (cek) format: #{alg} #{possible_envelope.inspect}"
|
|
214
|
+
raise Errors::DecryptionError, msg
|
|
215
|
+
end
|
|
216
|
+
unless POSSIBLE_WRAPPING_FORMATS.include? possible_envelope['x-amz-w']
|
|
217
|
+
alg = possible_envelope['x-amz-w'].inspect
|
|
218
|
+
msg = "unsupported key wrapping algorithm: #{alg}"
|
|
219
|
+
raise Errors::DecryptionError, msg
|
|
220
|
+
end
|
|
221
|
+
unless (missing_keys = REQUIRED_ENVELOPE_KEYS - possible_envelope.keys).empty?
|
|
222
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#determining-s3ec-object-status
|
|
223
|
+
##% In general, if there is any deviation from the above format, with the exception of additional unrelated mapkeys, then the S3EC SHOULD throw an exception.
|
|
224
|
+
msg = "incomplete v3 encryption envelope:\n"
|
|
225
|
+
msg += " missing: #{missing_keys.join(',')}\n"
|
|
226
|
+
raise Errors::DecryptionError, msg
|
|
227
|
+
end
|
|
228
|
+
possible_envelope
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
|
|
237
|
+
##= type=exception
|
|
238
|
+
##= reason=This has never been supported in Ruby
|
|
239
|
+
##% This material description string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server.
|
|
240
|
+
|
|
241
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
|
|
242
|
+
##= type=exception
|
|
243
|
+
##= reason=This has never been supported in Ruby
|
|
244
|
+
##% This encryption context string MAY be encoded by the esoteric double-encoding scheme used by the S3 web server.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module Aws
|
|
6
|
+
module S3
|
|
7
|
+
module EncryptionV3
|
|
8
|
+
# @api private
|
|
9
|
+
class DefaultCipherProvider
|
|
10
|
+
def initialize(options = {})
|
|
11
|
+
@key_provider = options[:key_provider]
|
|
12
|
+
@key_wrap_schema = validate_key_wrap(
|
|
13
|
+
options[:key_wrap_schema],
|
|
14
|
+
@key_provider.encryption_materials.key
|
|
15
|
+
)
|
|
16
|
+
##= ../specification/s3-encryption/encryption.md#content-encryption
|
|
17
|
+
##% The S3EC MUST use the encryption algorithm configured during [client](./client.md) initialization.
|
|
18
|
+
@content_encryption_schema = Utils.validate_cek(
|
|
19
|
+
options[:content_encryption_schema]
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :key_provider
|
|
24
|
+
|
|
25
|
+
# @return [Array<Hash,Cipher>] Creates an returns a new encryption
|
|
26
|
+
# envelope and encryption cipher.
|
|
27
|
+
def encryption_cipher(options = {})
|
|
28
|
+
validate_options(options)
|
|
29
|
+
data_key = Utils.generate_data_key
|
|
30
|
+
cipher, message_id, commitment_key = Utils.generate_alg_aes_256_gcm_hkdf_sha512_commit_key_cipher(data_key)
|
|
31
|
+
enc_key =
|
|
32
|
+
if @key_provider.encryption_materials.key.is_a? OpenSSL::PKey::RSA
|
|
33
|
+
encode64(
|
|
34
|
+
encrypt_rsa(data_key, @content_encryption_schema)
|
|
35
|
+
)
|
|
36
|
+
else
|
|
37
|
+
encode64(
|
|
38
|
+
encrypt_aes_gcm(data_key, @content_encryption_schema)
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#algorithm-suite-and-message-format-version-compatibility
|
|
42
|
+
##% Objects encrypted with ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY MUST use the V3 message format version only.
|
|
43
|
+
envelope = {
|
|
44
|
+
'x-amz-3' => enc_key,
|
|
45
|
+
'x-amz-c' => @content_encryption_schema,
|
|
46
|
+
'x-amz-w' => @key_wrap_schema,
|
|
47
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
|
|
48
|
+
##% The Material Description MUST be used for wrapping algorithms `AES/GCM` (`02`) and `RSA-OAEP-SHA1` (`22`).
|
|
49
|
+
'x-amz-m' => materials_description,
|
|
50
|
+
'x-amz-d' => encode64(commitment_key),
|
|
51
|
+
'x-amz-i' => encode64(message_id)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
[envelope, cipher]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Cipher] Given an encryption envelope, returns a
|
|
58
|
+
# decryption cipher.
|
|
59
|
+
def decryption_cipher(envelope, options = {})
|
|
60
|
+
validate_options(options)
|
|
61
|
+
wrapping_key = @key_provider.key_for(envelope['x-amz-m'])
|
|
62
|
+
|
|
63
|
+
data_key =
|
|
64
|
+
case envelope['x-amz-w']
|
|
65
|
+
when '02'
|
|
66
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
|
|
67
|
+
##% - The wrapping algorithm value "02" MUST be translated to AES/GCM upon retrieval, and vice versa on write.
|
|
68
|
+
if wrapping_key.is_a? OpenSSL::PKey::RSA
|
|
69
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
|
70
|
+
' with an RSA key and the x-amz-wrap-alg is AES/GCM.'
|
|
71
|
+
end
|
|
72
|
+
Utils.decrypt_aes_gcm(wrapping_key,
|
|
73
|
+
decode64(envelope['x-amz-3']),
|
|
74
|
+
@content_encryption_schema)
|
|
75
|
+
when '22'
|
|
76
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
|
|
77
|
+
##% - The wrapping algorithm value "22" MUST be translated to RSA-OAEP-SHA1 upon retrieval, and vice versa on write.
|
|
78
|
+
unless wrapping_key.is_a? OpenSSL::PKey::RSA
|
|
79
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
|
80
|
+
' with an AES key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
|
|
81
|
+
end
|
|
82
|
+
key, cek_alg = Utils.decrypt_rsa(wrapping_key, decode64(envelope['x-amz-3']))
|
|
83
|
+
raise Errors::CEKAlgMismatchError unless cek_alg == @content_encryption_schema
|
|
84
|
+
|
|
85
|
+
key
|
|
86
|
+
when '12'
|
|
87
|
+
raise ArgumentError, 'Key mismatch - Client is configured' \
|
|
88
|
+
' with a user provided key and the x-amz-w is' \
|
|
89
|
+
' kms+context. Please configure the client with the' \
|
|
90
|
+
' required kms_key_id'
|
|
91
|
+
else
|
|
92
|
+
raise ArgumentError, 'Unsupported wrapping algorithm: ' \
|
|
93
|
+
"#{envelope['x-amz-w']}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
message_id = decode64(envelope['x-amz-i'])
|
|
97
|
+
commitment_key = decode64(envelope['x-amz-d'])
|
|
98
|
+
|
|
99
|
+
Utils.derive_alg_aes_256_gcm_hkdf_sha512_commit_key_cipher(data_key, message_id, commitment_key)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Validate that the key_wrap_schema
|
|
105
|
+
# is valid, supported and matches the provided key.
|
|
106
|
+
# Returns the string version for the x-amz-key-wrap-alg
|
|
107
|
+
def validate_key_wrap(key_wrap_schema, key)
|
|
108
|
+
if key.is_a? OpenSSL::PKey::RSA
|
|
109
|
+
unless key_wrap_schema == :rsa_oaep_sha1
|
|
110
|
+
raise ArgumentError, ':key_wrap_schema must be set to :rsa_oaep_sha1 for RSA keys.'
|
|
111
|
+
end
|
|
112
|
+
else
|
|
113
|
+
unless key_wrap_schema == :aes_gcm
|
|
114
|
+
raise ArgumentError, ':key_wrap_schema must be set to :aes_gcm for AES keys.'
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
case key_wrap_schema
|
|
119
|
+
when :rsa_oaep_sha1 then '22'
|
|
120
|
+
when :aes_gcm then '02'
|
|
121
|
+
when :kms_context
|
|
122
|
+
raise ArgumentError, 'A kms_key_id is required when using :kms_context.'
|
|
123
|
+
else
|
|
124
|
+
raise ArgumentError, "Unsupported key_wrap_schema: #{key_wrap_schema}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def encrypt_aes_gcm(data, auth_data)
|
|
129
|
+
Utils.encrypt_aes_gcm(@key_provider.encryption_materials.key, data, auth_data)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def encrypt_rsa(data, auth_data)
|
|
133
|
+
Utils.encrypt_rsa(@key_provider.encryption_materials.key, data, auth_data)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def materials_description
|
|
137
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#v3-only
|
|
138
|
+
##% If the mapkey x-amz-m is not present, the default Material Description value MUST be set to an empty map (`{}`).
|
|
139
|
+
@key_provider.encryption_materials.description || {}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def encode64(str)
|
|
143
|
+
Base64.encode64(str).split("\n") * ''
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def decode64(str)
|
|
147
|
+
Base64.decode64(str)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def validate_options(options)
|
|
151
|
+
return if options[:kms_encryption_context].nil?
|
|
152
|
+
|
|
153
|
+
raise ArgumentError, 'Cannot provide :kms_encryption_context ' \
|
|
154
|
+
'with non KMS client.'
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
module S3
|
|
5
|
+
module EncryptionV3
|
|
6
|
+
# The default key provider is constructed with a single key
|
|
7
|
+
# that is used for both encryption and decryption, ignoring
|
|
8
|
+
# the possible per-object envelope encryption materials description.
|
|
9
|
+
# @api private
|
|
10
|
+
class DefaultKeyProvider
|
|
11
|
+
include KeyProvider
|
|
12
|
+
|
|
13
|
+
# @option options [required, OpenSSL::PKey::RSA, String] :encryption_key
|
|
14
|
+
# The master key to use for encrypting objects.
|
|
15
|
+
# @option options [String<JSON>] :materials_description ('{}')
|
|
16
|
+
# A description of the encryption key.
|
|
17
|
+
def initialize(options = {})
|
|
18
|
+
@encryption_materials = Materials.new(
|
|
19
|
+
key: options[:encryption_key],
|
|
20
|
+
description: options[:materials_description] || '{}'
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Materials]
|
|
25
|
+
attr_reader :encryption_materials
|
|
26
|
+
|
|
27
|
+
# @param [String<JSON>] materials_description
|
|
28
|
+
# @return Returns the key given in the constructor.
|
|
29
|
+
def key_for(_materials_description)
|
|
30
|
+
@encryption_materials.key
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module Aws
|
|
6
|
+
module S3
|
|
7
|
+
module EncryptionV3
|
|
8
|
+
# @api private
|
|
9
|
+
class EncryptHandler < Seahorse::Client::Handler
|
|
10
|
+
def call(context)
|
|
11
|
+
envelope, cipher = context[:encryption][:cipher_provider]
|
|
12
|
+
.encryption_cipher(
|
|
13
|
+
kms_encryption_context: context[:encryption][:kms_encryption_context]
|
|
14
|
+
)
|
|
15
|
+
context[:encryption][:cipher] = cipher
|
|
16
|
+
apply_encryption_envelope(context, envelope)
|
|
17
|
+
apply_encryption_cipher(context, cipher)
|
|
18
|
+
apply_cse_user_agent(context)
|
|
19
|
+
@handler.call(context)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def apply_encryption_envelope(context, envelope)
|
|
25
|
+
##= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
|
|
26
|
+
##% The S3EC MUST support writing some or all (depending on format) content metadata to an Instruction File.
|
|
27
|
+
if context[:encryption][:envelope_location] == :instruction_file
|
|
28
|
+
suffix = context[:encryption][:instruction_file_suffix]
|
|
29
|
+
instruction_envelop, metadata_envelop = split_for_instruction_file(envelope)
|
|
30
|
+
|
|
31
|
+
context.client.put_object(
|
|
32
|
+
bucket: context.params[:bucket],
|
|
33
|
+
key: context.params[:key] + suffix,
|
|
34
|
+
##= ../specification/s3-encryption/data-format/metadata-strategy.md#instruction-file
|
|
35
|
+
##% The content metadata stored in the Instruction File MUST be serialized to a JSON string.
|
|
36
|
+
##% The serialized JSON string MUST be the only contents of the Instruction File.
|
|
37
|
+
body: Json.dump(instruction_envelop)
|
|
38
|
+
)
|
|
39
|
+
context.params[:metadata] ||= {}
|
|
40
|
+
context.params[:metadata].update(metadata_envelop)
|
|
41
|
+
else
|
|
42
|
+
context.params[:metadata] ||= {}
|
|
43
|
+
context.params[:metadata].update(envelope)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def apply_encryption_cipher(context, cipher)
|
|
48
|
+
io = context.params[:body] || ''
|
|
49
|
+
io = StringIO.new(io) if io.is_a? String
|
|
50
|
+
context.params[:body] = IOEncrypter.new(cipher, io)
|
|
51
|
+
context.params[:metadata] ||= {}
|
|
52
|
+
# Leaving this in because even though this is years old
|
|
53
|
+
# it is still important to *not* MD5 the plaintext
|
|
54
|
+
# If there exists any old integration points still doing this
|
|
55
|
+
# that upgrade from 1 to 3 this needs to still fail.
|
|
56
|
+
if context.params.delete(:content_md5)
|
|
57
|
+
raise ArgumentError, 'Setting content_md5 on client side '\
|
|
58
|
+
'encrypted objects is deprecated.'
|
|
59
|
+
end
|
|
60
|
+
context.http_response.on_headers do
|
|
61
|
+
context.params[:body].close
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def apply_cse_user_agent(context)
|
|
66
|
+
if context.config.user_agent_suffix.nil?
|
|
67
|
+
context.config.user_agent_suffix = EC_USER_AGENT
|
|
68
|
+
elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
|
|
69
|
+
context.config.user_agent_suffix += " #{EC_USER_AGENT}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def split_for_instruction_file(envelop)
|
|
74
|
+
##= ../specification/s3-encryption/data-format/content-metadata.md#content-metadata-mapkeys
|
|
75
|
+
##% In the V3 format, the mapkeys "x-amz-c", "x-amz-d", and "x-amz-i" MUST be stored exclusively in the Object Metadata.
|
|
76
|
+
##= ../specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
|
|
77
|
+
##% - The V3 message format MUST store the mapkey "x-amz-c" and its value in the Object Metadata when writing with an Instruction File.
|
|
78
|
+
##% - The V3 message format MUST NOT store the mapkey "x-amz-c" and its value in the Instruction File.
|
|
79
|
+
##% - The V3 message format MUST store the mapkey "x-amz-d" and its value in the Object Metadata when writing with an Instruction File.
|
|
80
|
+
##% - The V3 message format MUST NOT store the mapkey "x-amz-d" and its value in the Instruction File.
|
|
81
|
+
##% - The V3 message format MUST store the mapkey "x-amz-i" and its value in the Object Metadata when writing with an Instruction File.
|
|
82
|
+
##% - The V3 message format MUST NOT store the mapkey "x-amz-i" and its value in the Instruction File.
|
|
83
|
+
metadata_envelop = envelop.select { |k, _v| Decryption::METADATA_KEY.include?(k) }
|
|
84
|
+
# Exclude the metadata keys rather than include the envelop keys
|
|
85
|
+
# because there might be additional information
|
|
86
|
+
##= ../specification/s3-encryption/data-format/metadata-strategy.md#v3-instruction-files
|
|
87
|
+
##% - The V3 message format MUST store the mapkey "x-amz-3" and its value in the Instruction File.
|
|
88
|
+
##% - The V3 message format MUST store the mapkey "x-amz-w" and its value in the Instruction File.
|
|
89
|
+
##% - The V3 message format MUST store the mapkey "x-amz-m" and its value (when present in the content metadata) in the Instruction File.
|
|
90
|
+
##% - The V3 message format MUST store the mapkey "x-amz-t" and its value (when present in the content metadata) in the Instruction File.
|
|
91
|
+
instruction_envelop = envelop.reject { |k, _v| Decryption::METADATA_KEY.include?(k) }
|
|
92
|
+
|
|
93
|
+
[instruction_envelop, metadata_envelop]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|