aws-sdk-resources 2.11.561 → 2.11.562

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aws-sdk-resources/services/s3.rb +1 -0
  3. data/lib/aws-sdk-resources/services/s3/encryption.rb +3 -0
  4. data/lib/aws-sdk-resources/services/s3/encryption/client.rb +24 -7
  5. data/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb +65 -25
  6. data/lib/aws-sdk-resources/services/s3/encryption/default_cipher_provider.rb +43 -5
  7. data/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb +2 -0
  8. data/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb +13 -2
  9. data/lib/aws-sdk-resources/services/s3/encryption/errors.rb +2 -0
  10. data/lib/aws-sdk-resources/services/s3/encryption/io_auth_decrypter.rb +2 -0
  11. data/lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb +8 -1
  12. data/lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb +2 -0
  13. data/lib/aws-sdk-resources/services/s3/encryption/key_provider.rb +2 -0
  14. data/lib/aws-sdk-resources/services/s3/encryption/kms_cipher_provider.rb +36 -3
  15. data/lib/aws-sdk-resources/services/s3/encryption/materials.rb +8 -6
  16. data/lib/aws-sdk-resources/services/s3/encryption/utils.rb +25 -0
  17. data/lib/aws-sdk-resources/services/s3/encryptionV2/client.rb +559 -0
  18. data/lib/aws-sdk-resources/services/s3/encryptionV2/decrypt_handler.rb +214 -0
  19. data/lib/aws-sdk-resources/services/s3/encryptionV2/default_cipher_provider.rb +170 -0
  20. data/lib/aws-sdk-resources/services/s3/encryptionV2/default_key_provider.rb +40 -0
  21. data/lib/aws-sdk-resources/services/s3/encryptionV2/encrypt_handler.rb +69 -0
  22. data/lib/aws-sdk-resources/services/s3/encryptionV2/errors.rb +37 -0
  23. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_auth_decrypter.rb +58 -0
  24. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_decrypter.rb +37 -0
  25. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_encrypter.rb +73 -0
  26. data/lib/aws-sdk-resources/services/s3/encryptionV2/key_provider.rb +31 -0
  27. data/lib/aws-sdk-resources/services/s3/encryptionV2/kms_cipher_provider.rb +169 -0
  28. data/lib/aws-sdk-resources/services/s3/encryptionV2/materials.rb +60 -0
  29. data/lib/aws-sdk-resources/services/s3/encryptionV2/utils.rb +103 -0
  30. data/lib/aws-sdk-resources/services/s3/encryption_v2.rb +24 -0
  31. metadata +18 -4
@@ -0,0 +1,214 @@
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
+
11
+ V1_ENVELOPE_KEYS = %w(
12
+ x-amz-key
13
+ x-amz-iv
14
+ x-amz-matdesc
15
+ )
16
+
17
+ V2_ENVELOPE_KEYS = %w(
18
+ x-amz-key-v2
19
+ x-amz-iv
20
+ x-amz-cek-alg
21
+ x-amz-wrap-alg
22
+ x-amz-matdesc
23
+ )
24
+
25
+ V2_OPTIONAL_KEYS = %w(x-amz-tag-len)
26
+
27
+ POSSIBLE_ENVELOPE_KEYS = (V1_ENVELOPE_KEYS +
28
+ V2_ENVELOPE_KEYS + V2_OPTIONAL_KEYS).uniq
29
+
30
+ POSSIBLE_WRAPPING_FORMATS = %w(
31
+ AES/GCM
32
+ kms
33
+ kms+context
34
+ RSA-OAEP-SHA1
35
+ )
36
+
37
+ POSSIBLE_ENCRYPTION_FORMATS = %w(
38
+ AES/GCM/NoPadding
39
+ AES/CBC/PKCS5Padding
40
+ AES/CBC/PKCS7Padding
41
+ )
42
+
43
+ AUTH_REQUIRED_CEK_ALGS = %w(AES/GCM/NoPadding)
44
+
45
+ def call(context)
46
+ attach_http_event_listeners(context)
47
+ apply_cse_user_agent(context)
48
+ @handler.call(context)
49
+ end
50
+
51
+ private
52
+
53
+ def attach_http_event_listeners(context)
54
+
55
+ context.http_response.on_headers(200) do
56
+ cipher, envelope = decryption_cipher(context)
57
+ decrypter = body_contains_auth_tag?(envelope) ?
58
+ authenticated_decrypter(context, cipher, envelope) :
59
+ IODecrypter.new(cipher, context.http_response.body)
60
+ context.http_response.body = decrypter
61
+ end
62
+
63
+ context.http_response.on_success(200) do
64
+ decrypter = context.http_response.body
65
+ decrypter.finalize
66
+ decrypter.io.rewind if decrypter.io.respond_to?(:rewind)
67
+ context.http_response.body = decrypter.io
68
+ end
69
+
70
+ context.http_response.on_error do
71
+ if context.http_response.body.respond_to?(:io)
72
+ context.http_response.body = context.http_response.body.io
73
+ end
74
+ end
75
+ end
76
+
77
+ def decryption_cipher(context)
78
+ if (envelope = get_encryption_envelope(context))
79
+ cipher = context[:encryption][:cipher_provider]
80
+ .decryption_cipher(
81
+ envelope,
82
+ context[:encryption]
83
+ )
84
+ [cipher, envelope]
85
+ else
86
+ raise Errors::DecryptionError, "unable to locate encryption envelope"
87
+ end
88
+ end
89
+
90
+ def get_encryption_envelope(context)
91
+ if context[:encryption][:envelope_location] == :metadata
92
+ envelope_from_metadata(context) || envelope_from_instr_file(context)
93
+ else
94
+ envelope_from_instr_file(context) || envelope_from_metadata(context)
95
+ end
96
+ end
97
+
98
+ def envelope_from_metadata(context)
99
+ possible_envelope = {}
100
+ POSSIBLE_ENVELOPE_KEYS.each do |suffix|
101
+ if value = context.http_response.headers["x-amz-meta-#{suffix}"]
102
+ possible_envelope[suffix] = value
103
+ end
104
+ end
105
+ extract_envelope(possible_envelope)
106
+ end
107
+
108
+ def envelope_from_instr_file(context)
109
+ suffix = context[:encryption][:instruction_file_suffix]
110
+ possible_envelope = Json.load(context.client.get_object(
111
+ bucket: context.params[:bucket],
112
+ key: context.params[:key] + suffix
113
+ ).body.read)
114
+ extract_envelope(possible_envelope)
115
+ rescue S3::Errors::ServiceError, Json::ParseError
116
+ nil
117
+ end
118
+
119
+ def extract_envelope(hash)
120
+ return nil unless hash
121
+ return v1_envelope(hash) if hash.key?('x-amz-key')
122
+ return v2_envelope(hash) if hash.key?('x-amz-key-v2')
123
+ if hash.keys.any? { |key| key.match(/^x-amz-key-(.+)$/) }
124
+ msg = "unsupported envelope encryption version #{$1}"
125
+ raise Errors::DecryptionError, msg
126
+ end
127
+ end
128
+
129
+ def v1_envelope(envelope)
130
+ envelope
131
+ end
132
+
133
+ def v2_envelope(envelope)
134
+ unless POSSIBLE_ENCRYPTION_FORMATS.include? envelope['x-amz-cek-alg']
135
+ alg = envelope['x-amz-cek-alg'].inspect
136
+ msg = "unsupported content encrypting key (cek) format: #{alg}"
137
+ raise Errors::DecryptionError, msg
138
+ end
139
+ unless POSSIBLE_WRAPPING_FORMATS.include? envelope['x-amz-wrap-alg']
140
+ alg = envelope['x-amz-wrap-alg'].inspect
141
+ msg = "unsupported key wrapping algorithm: #{alg}"
142
+ raise Errors::DecryptionError, msg
143
+ end
144
+ unless (missing_keys = V2_ENVELOPE_KEYS - envelope.keys).empty?
145
+ msg = "incomplete v2 encryption envelope:\n"
146
+ msg += " missing: #{missing_keys.join(',')}\n"
147
+ raise Errors::DecryptionError, msg
148
+ end
149
+ envelope
150
+ end
151
+
152
+ # This method fetches the tag from the end of the object by
153
+ # making a GET Object w/range request. This auth tag is used
154
+ # to initialize the cipher, and the decrypter truncates the
155
+ # auth tag from the body when writing the final bytes.
156
+ def authenticated_decrypter(context, cipher, envelope)
157
+ if RUBY_VERSION.match(/1.9/)
158
+ raise "authenticated decryption not supported by OpenSSL in Ruby version ~> 1.9"
159
+ raise Aws::Errors::NonSupportedRubyVersionError, msg
160
+ end
161
+ http_resp = context.http_response
162
+ content_length = http_resp.headers['content-length'].to_i
163
+ auth_tag_length = auth_tag_length(envelope)
164
+
165
+ auth_tag = context.client.get_object(
166
+ bucket: context.params[:bucket],
167
+ key: context.params[:key],
168
+ range: "bytes=-#{auth_tag_length}"
169
+ ).body.read
170
+
171
+ cipher.auth_tag = auth_tag
172
+ cipher.auth_data = ''
173
+
174
+ # The encrypted object contains both the cipher text
175
+ # plus a trailing auth tag.
176
+ IOAuthDecrypter.new(
177
+ io: http_resp.body,
178
+ encrypted_content_length: content_length - auth_tag_length,
179
+ cipher: cipher)
180
+ end
181
+
182
+ def body_contains_auth_tag?(envelope)
183
+ AUTH_REQUIRED_CEK_ALGS.include?(envelope['x-amz-cek-alg'])
184
+ end
185
+
186
+ # Determine the auth tag length from the algorithm
187
+ # Validate it against the value provided in the x-amz-tag-len
188
+ # Return the tag length in bytes
189
+ def auth_tag_length(envelope)
190
+ tag_length =
191
+ case envelope['x-amz-cek-alg']
192
+ when 'AES/GCM/NoPadding' then AES_GCM_TAG_LEN_BYTES
193
+ else
194
+ raise ArgumentError, 'Unsupported cek-alg: ' \
195
+ "#{envelope['x-amz-cek-alg']}"
196
+ end
197
+ if (tag_length * 8) != envelope['x-amz-tag-len'].to_i
198
+ raise Errors::DecryptionError, 'x-amz-tag-len does not match expected'
199
+ end
200
+ tag_length
201
+ end
202
+
203
+ def apply_cse_user_agent(context)
204
+ if context.config.user_agent_suffix.nil?
205
+ context.config.user_agent_suffix = EC_USER_AGENT
206
+ elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
207
+ context.config.user_agent_suffix += " #{EC_USER_AGENT}"
208
+ end
209
+ end
210
+
211
+ end
212
+ end
213
+ end
214
+ 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,69 @@
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
+ if RUBY_VERSION.match(/1.9/)
13
+ raise "authenticated encryption not supported by OpenSSL in Ruby version ~> 1.9"
14
+ raise Aws::Errors::NonSupportedRubyVersionError, msg
15
+ end
16
+ envelope, cipher = context[:encryption][:cipher_provider]
17
+ .encryption_cipher(
18
+ kms_encryption_context: context[:encryption][:kms_encryption_context]
19
+ )
20
+ context[:encryption][:cipher] = cipher
21
+ apply_encryption_envelope(context, envelope)
22
+ apply_encryption_cipher(context, cipher)
23
+ apply_cse_user_agent(context)
24
+ @handler.call(context)
25
+ end
26
+
27
+ private
28
+
29
+ def apply_encryption_envelope(context, envelope)
30
+ if context[:encryption][:envelope_location] == :instruction_file
31
+ suffix = context[:encryption][:instruction_file_suffix]
32
+ context.client.put_object(
33
+ bucket: context.params[:bucket],
34
+ key: context.params[:key] + suffix,
35
+ body: Json.dump(envelope)
36
+ )
37
+ else # :metadata
38
+ context.params[:metadata] ||= {}
39
+ context.params[:metadata].update(envelope)
40
+ end
41
+ end
42
+
43
+ def apply_encryption_cipher(context, cipher)
44
+ io = context.params[:body] || ''
45
+ io = StringIO.new(io) if io.is_a? String
46
+ context.params[:body] = IOEncrypter.new(cipher, io)
47
+ context.params[:metadata] ||= {}
48
+ context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
49
+ if context.params.delete(:content_md5)
50
+ raise ArgumentError, 'Setting content_md5 on client side '\
51
+ 'encrypted objects is deprecated.'
52
+ end
53
+ context.http_response.on_headers do
54
+ context.params[:body].close
55
+ end
56
+ end
57
+
58
+ def apply_cse_user_agent(context)
59
+ if context.config.user_agent_suffix.nil?
60
+ context.config.user_agent_suffix = EC_USER_AGENT
61
+ elsif !context.config.user_agent_suffix.include? EC_USER_AGENT
62
+ context.config.user_agent_suffix += " #{EC_USER_AGENT}"
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end