1 /*
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */

15
16 package software.amazon.awssdk.auth.signer;
17
18 import static software.amazon.awssdk.auth.signer.internal.SignerConstant.X_AMZ_CONTENT_SHA256;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.util.Optional;
23 import software.amazon.awssdk.annotations.SdkPublicApi;
24 import software.amazon.awssdk.auth.credentials.CredentialUtils;
25 import software.amazon.awssdk.auth.signer.internal.AbstractAws4Signer;
26 import software.amazon.awssdk.auth.signer.internal.Aws4SignerRequestParams;
27 import software.amazon.awssdk.auth.signer.internal.AwsChunkedEncodingInputStream;
28 import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams;
29 import software.amazon.awssdk.auth.signer.params.AwsS3V4SignerParams;
30 import software.amazon.awssdk.core.exception.SdkClientException;
31 import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
32 import software.amazon.awssdk.http.ContentStreamProvider;
33 import software.amazon.awssdk.http.SdkHttpFullRequest;
34 import software.amazon.awssdk.utils.BinaryUtils;
35
36 /**
37  * AWS4 signer implementation for AWS S3
38  */

39 @SdkPublicApi
40 public final class AwsS3V4Signer extends AbstractAws4Signer<AwsS3V4SignerParams, Aws4PresignerParams> {
41
42     private static final String CONTENT_SHA_256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
43
44     /**
45      * Sent to S3 in lieu of a payload hash when unsigned payloads are enabled
46      */

47     private static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
48     private static final String CONTENT_LENGTH = "Content-Length";
49
50     private AwsS3V4Signer() {
51     }
52
53     public static AwsS3V4Signer create() {
54         return new AwsS3V4Signer();
55     }
56
57     @Override
58     public SdkHttpFullRequest sign(SdkHttpFullRequest request, ExecutionAttributes executionAttributes) {
59         AwsS3V4SignerParams signingParams = constructAwsS3SignerParams(executionAttributes);
60
61         return sign(request, signingParams);
62     }
63
64     /**
65      * A method to sign the given #request. The parameters required for signing are provided through the modeled
66      * {@link AwsS3V4Signer} class.
67      *
68      * @param request The request to sign
69      * @param signingParams Class with the parameters used for signing the request
70      * @return A signed version of the input request
71      */

72     public SdkHttpFullRequest sign(SdkHttpFullRequest request, AwsS3V4SignerParams signingParams) {
73         // anonymous credentials, don't sign
74         if (CredentialUtils.isAnonymous(signingParams.awsCredentials())) {
75             return request;
76         }
77
78         Aws4SignerRequestParams requestParams = new Aws4SignerRequestParams(signingParams);
79
80         return doSign(request, requestParams, signingParams).build();
81     }
82
83     private AwsS3V4SignerParams constructAwsS3SignerParams(ExecutionAttributes executionAttributes) {
84         AwsS3V4SignerParams.Builder signerParams = extractSignerParams(AwsS3V4SignerParams.builder(),
85                                                                              executionAttributes);
86
87         Optional.ofNullable(executionAttributes.getAttribute(S3SignerExecutionAttribute.ENABLE_CHUNKED_ENCODING))
88                 .ifPresent(signerParams::enableChunkedEncoding);
89
90         Optional.ofNullable(executionAttributes.getAttribute(S3SignerExecutionAttribute.ENABLE_PAYLOAD_SIGNING))
91                 .ifPresent(signerParams::enablePayloadSigning);
92
93         return signerParams.build();
94     }
95
96     @Override
97     public SdkHttpFullRequest presign(SdkHttpFullRequest request, ExecutionAttributes executionAttributes) {
98         Aws4PresignerParams signingParams =
99             extractPresignerParams(Aws4PresignerParams.builder(), executionAttributes).build();
100
101         return presign(request, signingParams);
102     }
103
104     /**
105      * A method to pre sign the given #request. The parameters required for pre signing are provided through the modeled
106      * {@link Aws4PresignerParams} class.
107      *
108      * @param request The request to pre-sign
109      * @param signingParams Class with the parameters used for pre signing the request
110      * @return A pre signed version of the input request
111      */

112     public SdkHttpFullRequest presign(SdkHttpFullRequest request, Aws4PresignerParams signingParams) {
113         // anonymous credentials, don't sign
114         if (CredentialUtils.isAnonymous(signingParams.awsCredentials())) {
115             return request;
116         }
117
118         Aws4SignerRequestParams requestParams = new Aws4SignerRequestParams(signingParams);
119
120         return doPresign(request, requestParams, signingParams).build();
121     }
122
123     /**
124      * If necessary, creates a chunk-encoding wrapper on the request payload.
125      */

126     @Override
127     protected void processRequestPayload(SdkHttpFullRequest.Builder mutableRequest,
128                                          byte[] signature,
129                                          byte[] signingKey,
130                                          Aws4SignerRequestParams signerRequestParams,
131                                          AwsS3V4SignerParams signerParams) {
132
133         if (useChunkEncoding(mutableRequest, signerParams)) {
134             if (mutableRequest.contentStreamProvider() != null) {
135                 ContentStreamProvider streamProvider = mutableRequest.contentStreamProvider();
136                 mutableRequest.contentStreamProvider(() -> AwsS3V4Signer.this.asChunkEncodedStream(
137                         streamProvider.newStream(),
138                         signature,
139                         signingKey,
140                         signerRequestParams
141                 ));
142             }
143         }
144     }
145
146     @Override
147     protected String calculateContentHashPresign(SdkHttpFullRequest.Builder mutableRequest, Aws4PresignerParams signerParams) {
148         return UNSIGNED_PAYLOAD;
149     }
150
151     private AwsChunkedEncodingInputStream asChunkEncodedStream(InputStream inputStream,
152                                                                byte[] signature,
153                                                                byte[] signingKey,
154                                                                Aws4SignerRequestParams signerRequestParams) {
155         return new AwsChunkedEncodingInputStream(
156                 inputStream,
157                 signingKey,
158                 signerRequestParams.getFormattedRequestSigningDateTime(),
159                 signerRequestParams.getScope(),
160                 BinaryUtils.toHex(signature), this);
161     }
162
163     /**
164      * Returns the pre-defined header value and set other necessary headers if
165      * the request needs to be chunk-encoded. Otherwise calls the superclass
166      * method which calculates the hash of the whole content for signing.
167      */

168     @Override
169     protected String calculateContentHash(SdkHttpFullRequest.Builder mutableRequest, AwsS3V4SignerParams signerParams) {
170         // To be consistent with other service clients using sig-v4,
171         // we just set the header as "required", and AWS4Signer.sign() will be
172         // notified to pick up the header value returned by this method.
173         mutableRequest.putHeader(X_AMZ_CONTENT_SHA256, "required");
174
175         if (isPayloadSigningEnabled(mutableRequest, signerParams)) {
176             if (useChunkEncoding(mutableRequest, signerParams)) {
177                 String contentLength = mutableRequest.firstMatchingHeader(CONTENT_LENGTH)
178                                                            .orElse(null);
179                 long originalContentLength;
180                 if (contentLength != null) {
181                     originalContentLength = Long.parseLong(contentLength);
182                 } else {
183                     /**
184                      * "Content-Length" header could be missing if the caller is
185                      * uploading a stream without setting Content-Length in
186                      * ObjectMetadata. Before using sigv4, we rely on HttpClient to
187                      * add this header by using BufferedHttpEntity when creating the
188                      * HttpRequest object. But now, we need this information
189                      * immediately for the signing process, so we have to cache the
190                      * stream here.
191                      */

192                     try {
193                         originalContentLength = getContentLength(mutableRequest);
194                     } catch (IOException e) {
195                         throw SdkClientException.builder()
196                                                 .message("Cannot get the content-length of the request content.")
197                                                 .cause(e)
198                                                 .build();
199                     }
200                 }
201                 mutableRequest.putHeader("x-amz-decoded-content-length", Long.toString(originalContentLength));
202                 // Make sure "Content-Length" header is not empty so that HttpClient
203                 // won't cache the stream again to recover Content-Length
204                 mutableRequest.putHeader(CONTENT_LENGTH, Long.toString(
205                     AwsChunkedEncodingInputStream.calculateStreamContentLength(originalContentLength)));
206                 return CONTENT_SHA_256;
207             } else {
208                 return super.calculateContentHash(mutableRequest, signerParams);
209             }
210         }
211
212         return UNSIGNED_PAYLOAD;
213     }
214
215     /**
216      * Determine whether to use aws-chunked for signing
217      */

218     private boolean useChunkEncoding(SdkHttpFullRequest.Builder mutableRequest, AwsS3V4SignerParams signerParams) {
219         // Chunked encoding only makes sense to do when the payload is signed
220         return isPayloadSigningEnabled(mutableRequest, signerParams) && isChunkedEncodingEnabled(signerParams);
221     }
222
223     /**
224      * @return True if chunked encoding has been enabled. Otherwise false.
225      */

226     private boolean isChunkedEncodingEnabled(AwsS3V4SignerParams signerParams) {
227         Boolean isChunkedEncodingEnabled = signerParams.enableChunkedEncoding();
228         return isChunkedEncodingEnabled != null && isChunkedEncodingEnabled;
229     }
230
231     /**
232      * @return True if payload signing is explicitly enabled.
233      */

234     private boolean isPayloadSigningEnabled(SdkHttpFullRequest.Builder request, AwsS3V4SignerParams signerParams) {
235         /**
236          * If we aren't using https we should always sign the payload unless there is no payload
237          */

238         if (!request.protocol().equals("https") && request.contentStreamProvider() != null) {
239             return true;
240         }
241
242         Boolean isPayloadSigningEnabled = signerParams.enablePayloadSigning();
243         return isPayloadSigningEnabled != null && isPayloadSigningEnabled;
244     }
245
246     /**
247      * Read the content of the request to get the length of the stream.
248      */

249     private static long getContentLength(SdkHttpFullRequest.Builder requestBuilder) throws IOException {
250         InputStream content = requestBuilder.contentStreamProvider().newStream();
251
252         long contentLength = 0;
253         byte[] tmp = new byte[4096];
254         int read;
255         while ((read = content.read(tmp)) != -1) {
256             contentLength += read;
257         }
258         return contentLength;
259     }
260 }
261