aws-sdk-resources 2.11.559 → 2.11.564

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.
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 +77 -26
  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 +11 -3
  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 +561 -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