aws-sdk-resources 2.11.557 → 2.11.562
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-resources/services/s3.rb +1 -0
- data/lib/aws-sdk-resources/services/s3/encryption.rb +3 -0
- data/lib/aws-sdk-resources/services/s3/encryption/client.rb +24 -7
- data/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb +65 -25
- data/lib/aws-sdk-resources/services/s3/encryption/default_cipher_provider.rb +43 -5
- data/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb +13 -2
- data/lib/aws-sdk-resources/services/s3/encryption/errors.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/io_auth_decrypter.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb +8 -1
- data/lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/key_provider.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/kms_cipher_provider.rb +36 -3
- data/lib/aws-sdk-resources/services/s3/encryption/materials.rb +8 -6
- data/lib/aws-sdk-resources/services/s3/encryption/utils.rb +25 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/client.rb +559 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/decrypt_handler.rb +214 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/default_cipher_provider.rb +170 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/default_key_provider.rb +40 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/encrypt_handler.rb +69 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/errors.rb +37 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_auth_decrypter.rb +58 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_decrypter.rb +37 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_encrypter.rb +73 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/key_provider.rb +31 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/kms_cipher_provider.rb +169 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/materials.rb +60 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/utils.rb +103 -0
- data/lib/aws-sdk-resources/services/s3/encryption_v2.rb +24 -0
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6ac0e253aa72fe0c66930c23a50c0f1e944e79e500febf995d11aba32bc80a7
|
4
|
+
data.tar.gz: 8f60593f8927f98912a6655231a22cdfadc747e276d0f1ef277835f0a8dc3163
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6d6b6eddfead95cc30c36f3d6d760353edcea6387d4213c2164d5b5afa063836cf24c929e9f1d9efa4f1ce8b0a1ca1d53b6376531d494c11e38cf0056f57ac2
|
7
|
+
data.tar.gz: 4df465e650e56888d180f7c9b3adea51f4be02b5b456eb971a4c8193948aa0dd6a67cec90bc35cc3bc0b654599dbc2365fad31247df232d2b637dea7e9575287
|
@@ -7,6 +7,7 @@ module Aws
|
|
7
7
|
require 'aws-sdk-resources/services/s3/multipart_upload'
|
8
8
|
|
9
9
|
autoload :Encryption, 'aws-sdk-resources/services/s3/encryption'
|
10
|
+
autoload :EncryptionV2, 'aws-sdk-resources/services/s3/encryption_v2'
|
10
11
|
autoload :FilePart, 'aws-sdk-resources/services/s3/file_part'
|
11
12
|
autoload :FileUploader, 'aws-sdk-resources/services/s3/file_uploader'
|
12
13
|
autoload :FileDownloader, 'aws-sdk-resources/services/s3/file_downloader'
|
@@ -2,6 +2,9 @@ module Aws
|
|
2
2
|
module S3
|
3
3
|
module Encryption
|
4
4
|
|
5
|
+
AES_GCM_TAG_LEN_BYTES = 16
|
6
|
+
EC_USER_AGENT = 'S3CryptoV1n'
|
7
|
+
|
5
8
|
autoload :Client, 'aws-sdk-resources/services/s3/encryption/client'
|
6
9
|
autoload :DecryptHandler, 'aws-sdk-resources/services/s3/encryption/decrypt_handler'
|
7
10
|
autoload :DefaultCipherProvider, 'aws-sdk-resources/services/s3/encryption/default_cipher_provider'
|
@@ -1,6 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
1
5
|
module Aws
|
2
6
|
module S3
|
3
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}.
|
4
12
|
# Provides an encryption client that encrypts and decrypts data client-side,
|
5
13
|
# storing the encrypted data in Amazon S3.
|
6
14
|
#
|
@@ -26,7 +34,7 @@ module Aws
|
|
26
34
|
# data client-side.
|
27
35
|
#
|
28
36
|
# One of the benefits of envelope encryption is that if your master key
|
29
|
-
# is compromised, you have the option of
|
37
|
+
# is compromised, you have the option of just re-encrypting the stored
|
30
38
|
# envelope symmetric keys, instead of re-encrypting all of the
|
31
39
|
# data in your account.
|
32
40
|
#
|
@@ -178,15 +186,17 @@ module Aws
|
|
178
186
|
class Client
|
179
187
|
|
180
188
|
extend Deprecations
|
189
|
+
extend Forwardable
|
190
|
+
def_delegators :@client, :config, :delete_object, :head_object, :build_request
|
181
191
|
|
182
|
-
# Creates a new encryption client. You must provide
|
192
|
+
# Creates a new encryption client. You must provide one of the following
|
183
193
|
# options:
|
184
194
|
#
|
185
195
|
# * `:encryption_key`
|
186
196
|
# * `:kms_key_id`
|
187
197
|
# * `:key_provider`
|
188
198
|
#
|
189
|
-
# You may also pass any other options accepted by
|
199
|
+
# You may also pass any other options accepted by `Client#initialize`.
|
190
200
|
#
|
191
201
|
# @option options [S3::Client] :client A basic S3 client that is used
|
192
202
|
# to make api calls. If a `:client` is not provided, a new {S3::Client}
|
@@ -223,6 +233,13 @@ module Aws
|
|
223
233
|
@envelope_location = extract_location(options)
|
224
234
|
@instruction_file_suffix = extract_suffix(options)
|
225
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
|
+
|
226
243
|
|
227
244
|
# @return [S3::Client]
|
228
245
|
attr_reader :client
|
@@ -327,7 +344,7 @@ module Aws
|
|
327
344
|
elsif options[:encryption_key]
|
328
345
|
DefaultKeyProvider.new(options)
|
329
346
|
else
|
330
|
-
msg =
|
347
|
+
msg = 'you must pass a :kms_key_id, :key_provider, or :encryption_key'
|
331
348
|
raise ArgumentError, msg
|
332
349
|
end
|
333
350
|
end
|
@@ -347,8 +364,8 @@ module Aws
|
|
347
364
|
if [:metadata, :instruction_file].include?(location)
|
348
365
|
location
|
349
366
|
else
|
350
|
-
msg =
|
351
|
-
|
367
|
+
msg = ':envelope_location must be :metadata or :instruction_file '\
|
368
|
+
"got #{location.inspect}"
|
352
369
|
raise ArgumentError, msg
|
353
370
|
end
|
354
371
|
end
|
@@ -358,7 +375,7 @@ module Aws
|
|
358
375
|
if String === suffix
|
359
376
|
suffix
|
360
377
|
else
|
361
|
-
msg =
|
378
|
+
msg = ':instruction_file_suffix must be a String'
|
362
379
|
raise ArgumentError, msg
|
363
380
|
end
|
364
381
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
|
3
5
|
module Aws
|
@@ -20,15 +22,29 @@ module Aws
|
|
20
22
|
x-amz-matdesc
|
21
23
|
)
|
22
24
|
|
23
|
-
|
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
|
+
)
|
24
36
|
|
25
37
|
POSSIBLE_ENCRYPTION_FORMATS = %w(
|
26
38
|
AES/GCM/NoPadding
|
27
39
|
AES/CBC/PKCS5Padding
|
40
|
+
AES/CBC/PKCS7Padding
|
28
41
|
)
|
29
42
|
|
43
|
+
AUTH_REQUIRED_CEK_ALGS = %w(AES/GCM/NoPadding)
|
44
|
+
|
30
45
|
def call(context)
|
31
46
|
attach_http_event_listeners(context)
|
47
|
+
apply_cse_user_agent(context)
|
32
48
|
@handler.call(context)
|
33
49
|
end
|
34
50
|
|
@@ -37,9 +53,9 @@ module Aws
|
|
37
53
|
def attach_http_event_listeners(context)
|
38
54
|
|
39
55
|
context.http_response.on_headers(200) do
|
40
|
-
cipher = decryption_cipher(context)
|
41
|
-
decrypter = body_contains_auth_tag?(
|
42
|
-
authenticated_decrypter(context, cipher) :
|
56
|
+
cipher, envelope = decryption_cipher(context)
|
57
|
+
decrypter = body_contains_auth_tag?(envelope) ?
|
58
|
+
authenticated_decrypter(context, cipher, envelope) :
|
43
59
|
IODecrypter.new(cipher, context.http_response.body)
|
44
60
|
context.http_response.body = decrypter
|
45
61
|
end
|
@@ -60,7 +76,12 @@ module Aws
|
|
60
76
|
|
61
77
|
def decryption_cipher(context)
|
62
78
|
if envelope = get_encryption_envelope(context)
|
63
|
-
context[:encryption][:cipher_provider]
|
79
|
+
cipher = context[:encryption][:cipher_provider]
|
80
|
+
.decryption_cipher(
|
81
|
+
envelope,
|
82
|
+
kms_encryption_context: context[:encryption][:kms_encryption_context]
|
83
|
+
)
|
84
|
+
[cipher, envelope]
|
64
85
|
else
|
65
86
|
raise Errors::DecryptionError, "unable to locate encryption envelope"
|
66
87
|
end
|
@@ -96,13 +117,12 @@ module Aws
|
|
96
117
|
end
|
97
118
|
|
98
119
|
def extract_envelope(hash)
|
120
|
+
return nil unless hash
|
99
121
|
return v1_envelope(hash) if hash.key?('x-amz-key')
|
100
122
|
return v2_envelope(hash) if hash.key?('x-amz-key-v2')
|
101
123
|
if hash.keys.any? { |key| key.match(/^x-amz-key-(.+)$/) }
|
102
124
|
msg = "unsupported envelope encryption version #{$1}"
|
103
125
|
raise Errors::DecryptionError, msg
|
104
|
-
else
|
105
|
-
nil # no envelope found
|
106
126
|
end
|
107
127
|
end
|
108
128
|
|
@@ -116,35 +136,31 @@ module Aws
|
|
116
136
|
msg = "unsupported content encrypting key (cek) format: #{alg}"
|
117
137
|
raise Errors::DecryptionError, msg
|
118
138
|
end
|
119
|
-
unless envelope['x-amz-wrap-alg']
|
120
|
-
# possible to support
|
121
|
-
# RSA/ECB/OAEPWithSHA-256AndMGF1Padding
|
139
|
+
unless POSSIBLE_WRAPPING_FORMATS.include? envelope['x-amz-wrap-alg']
|
122
140
|
alg = envelope['x-amz-wrap-alg'].inspect
|
123
141
|
msg = "unsupported key wrapping algorithm: #{alg}"
|
124
142
|
raise Errors::DecryptionError, msg
|
125
143
|
end
|
126
|
-
unless V2_ENVELOPE_KEYS
|
144
|
+
unless (missing_keys = V2_ENVELOPE_KEYS - envelope.keys).empty?
|
127
145
|
msg = "incomplete v2 encryption envelope:\n"
|
128
|
-
msg += "
|
129
|
-
msg += " got: #{envelope_keys.join(', ')}"
|
146
|
+
msg += " missing: #{missing_keys.join(',')}\n"
|
130
147
|
raise Errors::DecryptionError, msg
|
131
148
|
end
|
132
149
|
envelope
|
133
150
|
end
|
134
151
|
|
135
|
-
# When the x-amz-meta-x-amz-tag-len header is present, it indicates
|
136
|
-
# that the body of this object has a trailing auth tag. The header
|
137
|
-
# indicates the length of that tag.
|
138
|
-
#
|
139
152
|
# This method fetches the tag from the end of the object by
|
140
153
|
# making a GET Object w/range request. This auth tag is used
|
141
154
|
# to initialize the cipher, and the decrypter truncates the
|
142
155
|
# auth tag from the body when writing the final bytes.
|
143
|
-
def authenticated_decrypter(context, cipher)
|
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
|
144
161
|
http_resp = context.http_response
|
145
162
|
content_length = http_resp.headers['content-length'].to_i
|
146
|
-
auth_tag_length =
|
147
|
-
auth_tag_length = auth_tag_length.to_i / 8
|
163
|
+
auth_tag_length = auth_tag_length(envelope)
|
148
164
|
|
149
165
|
auth_tag = context.client.get_object(
|
150
166
|
bucket: context.params[:bucket],
|
@@ -156,16 +172,40 @@ module Aws
|
|
156
172
|
cipher.auth_data = ''
|
157
173
|
|
158
174
|
# The encrypted object contains both the cipher text
|
159
|
-
# plus a trailing auth tag.
|
160
|
-
|
161
|
-
decrypter = IOAuthDecrypter.new(
|
175
|
+
# plus a trailing auth tag.
|
176
|
+
IOAuthDecrypter.new(
|
162
177
|
io: http_resp.body,
|
163
178
|
encrypted_content_length: content_length - auth_tag_length,
|
164
179
|
cipher: cipher)
|
165
180
|
end
|
166
181
|
|
167
|
-
def body_contains_auth_tag?(
|
168
|
-
|
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
|
169
209
|
end
|
170
210
|
|
171
211
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
|
3
5
|
module Aws
|
@@ -24,11 +26,48 @@ module Aws
|
|
24
26
|
|
25
27
|
# @return [Cipher] Given an encryption envelope, returns a
|
26
28
|
# decryption cipher.
|
27
|
-
def decryption_cipher(envelope)
|
29
|
+
def decryption_cipher(envelope, options = {})
|
28
30
|
master_key = @key_provider.key_for(envelope['x-amz-matdesc'])
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
32
71
|
end
|
33
72
|
|
34
73
|
private
|
@@ -56,7 +95,6 @@ module Aws
|
|
56
95
|
def decode64(str)
|
57
96
|
Base64.decode64(str)
|
58
97
|
end
|
59
|
-
|
60
98
|
end
|
61
99
|
end
|
62
100
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'base64'
|
2
4
|
|
3
5
|
module Aws
|
@@ -10,6 +12,7 @@ module Aws
|
|
10
12
|
envelope, cipher = context[:encryption][:cipher_provider].encryption_cipher
|
11
13
|
apply_encryption_envelope(context, envelope, cipher)
|
12
14
|
apply_encryption_cipher(context, cipher)
|
15
|
+
apply_cse_user_agent(context)
|
13
16
|
@handler.call(context)
|
14
17
|
end
|
15
18
|
|
@@ -36,14 +39,22 @@ module Aws
|
|
36
39
|
context.params[:body] = IOEncrypter.new(cipher, io)
|
37
40
|
context.params[:metadata] ||= {}
|
38
41
|
context.params[:metadata]['x-amz-unencrypted-content-length'] = io.size
|
39
|
-
if
|
40
|
-
|
42
|
+
if context.params.delete(:content_md5)
|
43
|
+
warn('Setting content_md5 on client side encrypted objects is deprecated')
|
41
44
|
end
|
42
45
|
context.http_response.on_headers do
|
43
46
|
context.params[:body].close
|
44
47
|
end
|
45
48
|
end
|
46
49
|
|
50
|
+
def apply_cse_user_agent(context)
|
51
|
+
if context.config.user_agent_suffix.nil?
|
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
|
+
end
|
56
|
+
end
|
57
|
+
|
47
58
|
end
|
48
59
|
end
|
49
60
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Aws
|
2
4
|
module S3
|
3
5
|
module Encryption
|
@@ -8,7 +10,8 @@ module Aws
|
|
8
10
|
# @param [IO#write] io An IO-like object that responds to `#write`.
|
9
11
|
def initialize(cipher, io)
|
10
12
|
@cipher = cipher.clone
|
11
|
-
|
13
|
+
# Ensure that IO is reset between retries
|
14
|
+
@io = io.tap { |io| io.truncate(0) if io.respond_to?(:truncate) }
|
12
15
|
end
|
13
16
|
|
14
17
|
# @return [#write]
|
@@ -23,6 +26,10 @@ module Aws
|
|
23
26
|
@io.write(@cipher.final)
|
24
27
|
end
|
25
28
|
|
29
|
+
def size
|
30
|
+
@io.size
|
31
|
+
end
|
32
|
+
|
26
33
|
end
|
27
34
|
end
|
28
35
|
end
|