1 /*
2  * Copyright 2010-2020 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 package com.amazonaws.auth;
16
17 import static com.amazonaws.util.StringUtils.UTF8;
18
19 import com.amazonaws.AmazonClientException;
20 import com.amazonaws.ReadLimitInfo;
21 import com.amazonaws.SDKGlobalTime;
22 import com.amazonaws.SdkClientException;
23 import com.amazonaws.SignableRequest;
24 import com.amazonaws.internal.SdkDigestInputStream;
25 import com.amazonaws.internal.SdkThreadLocalsRegistry;
26 import com.amazonaws.util.Base64;
27 import com.amazonaws.util.BinaryUtils;
28 import com.amazonaws.util.SdkHttpUtils;
29 import com.amazonaws.util.StringUtils;
30 import java.io.ByteArrayInputStream;
31 import java.io.ByteArrayOutputStream;
32 import java.io.InputStream;
33 import java.net.URI;
34 import java.security.DigestInputStream;
35 import java.security.MessageDigest;
36 import java.security.NoSuchAlgorithmException;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.Date;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.SortedMap;
43 import java.util.TreeMap;
44 import javax.crypto.Mac;
45 import javax.crypto.spec.SecretKeySpec;
46
47 /**
48  * Abstract base class for AWS signing protocol implementations. Provides
49  * utilities commonly needed by signing protocols such as computing
50  * canonicalized host names, query string parameters, etc.
51  * <p>
52  * Not intended to be sub-classed by developers.
53  */

54 public abstract class AbstractAWSSigner implements Signer {
55
56     public static final String EMPTY_STRING_SHA256_HEX;
57     private static final ThreadLocal<MessageDigest> SHA256_MESSAGE_DIGEST;
58
59     static {
60         SHA256_MESSAGE_DIGEST = SdkThreadLocalsRegistry.register(
61                 new ThreadLocal<MessageDigest>() {
62                     @Override
63                     protected MessageDigest initialValue() {
64                         try {
65                             return MessageDigest.getInstance("SHA-256");
66                         } catch (NoSuchAlgorithmException e) {
67                             throw new SdkClientException(
68                                     "Unable to get SHA256 Function"
69                                             + e.getMessage(), e);
70                         }
71                     }
72                 });
73         EMPTY_STRING_SHA256_HEX = BinaryUtils.toHex(doHash(""));
74     }
75
76     /**
77      * Computes an RFC 2104-compliant HMAC signature and returns the result as a
78      * Base64 encoded string.
79      */

80     protected String signAndBase64Encode(String data, String key,
81             SigningAlgorithm algorithm) throws SdkClientException {
82         return signAndBase64Encode(data.getBytes(UTF8), key, algorithm);
83     }
84
85     /**
86      * Computes an RFC 2104-compliant HMAC signature for an array of bytes and
87      * returns the result as a Base64 encoded string.
88      */

89     protected String signAndBase64Encode(byte[] data, String key,
90             SigningAlgorithm algorithm) throws SdkClientException {
91         try {
92             byte[] signature = sign(data, key.getBytes(UTF8), algorithm);
93             return Base64.encodeAsString(signature);
94         } catch (Exception e) {
95             throw new SdkClientException(
96                     "Unable to calculate a request signature: "
97                             + e.getMessage(), e);
98         }
99     }
100
101     public byte[] sign(String stringData, byte[] key,
102             SigningAlgorithm algorithm) throws SdkClientException {
103         try {
104             byte[] data = stringData.getBytes(UTF8);
105             return sign(data, key, algorithm);
106         } catch (Exception e) {
107             throw new SdkClientException(
108                     "Unable to calculate a request signature: "
109                             + e.getMessage(), e);
110         }
111     }
112
113     public byte[] signWithMac(String stringData, Mac mac) {
114         try {
115             return mac.doFinal(stringData.getBytes(UTF8));
116         } catch (Exception e) {
117             throw new SdkClientException(
118                     "Unable to calculate a request signature: "
119                             + e.getMessage(), e);
120         }
121     }
122
123     protected byte[] sign(byte[] data, byte[] key,
124             SigningAlgorithm algorithm) throws SdkClientException {
125         try {
126             Mac mac = algorithm.getMac();
127             mac.init(new SecretKeySpec(key, algorithm.toString()));
128             return mac.doFinal(data);
129         } catch (Exception e) {
130             throw new SdkClientException(
131                     "Unable to calculate a request signature: "
132                             + e.getMessage(), e);
133         }
134     }
135
136     /**
137      * Hashes the string contents (assumed to be UTF-8) using the SHA-256
138      * algorithm.
139      *
140      * @param text
141      *            The string to hash.
142      *
143      * @return The hashed bytes from the specified string.
144      *
145      * @throws SdkClientException
146      *             If the hash cannot be computed.
147      */

148     public byte[] hash(String text) throws SdkClientException {
149         return AbstractAWSSigner.doHash(text);
150     }
151
152     private static byte[] doHash(String text) throws SdkClientException {
153         try {
154             MessageDigest md = getMessageDigestInstance();
155             md.update(text.getBytes(UTF8));
156             return md.digest();
157         } catch (Exception e) {
158             throw new SdkClientException(
159                     "Unable to compute hash while signing request: "
160                             + e.getMessage(), e);
161         }
162     }
163
164     protected byte[] hash(InputStream input) throws SdkClientException {
165         try {
166             MessageDigest md = getMessageDigestInstance();
167             @SuppressWarnings("resource")
168             DigestInputStream digestInputStream = new SdkDigestInputStream(
169                     input, md);
170             byte[] buffer = new byte[1024];
171             while (digestInputStream.read(buffer) > -1)
172                 ;
173             return digestInputStream.getMessageDigest().digest();
174         } catch (Exception e) {
175             throw new SdkClientException(
176                     "Unable to compute hash while signing request: "
177                             + e.getMessage(), e);
178         }
179     }
180
181     /**
182      * Hashes the binary data using the SHA-256 algorithm.
183      *
184      * @param data
185      *            The binary data to hash.
186      *
187      * @return The hashed bytes from the specified data.
188      *
189      * @throws SdkClientException
190      *             If the hash cannot be computed.
191      */

192     public byte[] hash(byte[] data) throws SdkClientException {
193         try {
194             MessageDigest md =  getMessageDigestInstance();
195             md.update(data);
196             return md.digest();
197         } catch (Exception e) {
198             throw new SdkClientException(
199                     "Unable to compute hash while signing request: "
200                             + e.getMessage(), e);
201         }
202     }
203     /**
204      * Examines the specified query string parameters and returns a
205      * canonicalized form.
206      * <p>
207      * The canonicalized query string is formed by first sorting all the query
208      * string parameters, then URI encoding both the key and value and then
209      * joining them, in order, separating key value pairs with an '&'.
210      *
211      * @param parameters
212      *            The query string parameters to be canonicalized.
213      *
214      * @return A canonicalized form for the specified query string parameters.
215      */

216     protected String getCanonicalizedQueryString(Map<String, List<String>> parameters) {
217
218         final SortedMap<String, List<String>> sorted = new TreeMap<String, List<String>>();
219
220         /**
221          * Signing protocol expects the param values also to be sorted after url
222          * encoding in addition to sorted parameter names.
223          */

224         for (Map.Entry<String, List<String>> entry : parameters.entrySet()) {
225             final String encodedParamName = SdkHttpUtils.urlEncode(
226                     entry.getKey(), false);
227             final List<String> paramValues = entry.getValue();
228             final List<String> encodedValues = new ArrayList<String>(
229                     paramValues.size());
230             for (String value : paramValues) {
231                 encodedValues.add(SdkHttpUtils.urlEncode(value, false));
232             }
233             Collections.sort(encodedValues);
234             sorted.put(encodedParamName, encodedValues);
235
236         }
237
238         final StringBuilder result = new StringBuilder();
239         for(Map.Entry<String, List<String>> entry : sorted.entrySet()) {
240             for(String value : entry.getValue()) {
241                 if (result.length() > 0) {
242                     result.append("&");
243                 }
244                 result.append(entry.getKey())
245                       .append("=")
246                       .append(value);
247             }
248         }
249
250         return result.toString();
251     }
252
253     protected String getCanonicalizedQueryString(SignableRequest<?> request) {
254         /*
255          * If we're using POST and we don't have any request payload content,
256          * then any request query parameters will be sent as the payload, and
257          * not in the actual query string.
258          */

259         if (SdkHttpUtils.usePayloadForQueryParameters(request))
260             return "";
261         return this.getCanonicalizedQueryString(request.getParameters());
262     }
263
264     /**
265      * Returns the request's payload as binary data.
266      *
267      * @param request
268      *            The request
269      * @return The data from the request's payload, as binary data.
270      */

271     protected byte[] getBinaryRequestPayload(SignableRequest<?> request) {
272         if (SdkHttpUtils.usePayloadForQueryParameters(request)) {
273             String encodedParameters = SdkHttpUtils.encodeParameters(request);
274             if (encodedParameters == null)
275                 return new byte[0];
276
277             return encodedParameters.getBytes(UTF8);
278         }
279
280         return getBinaryRequestPayloadWithoutQueryParams(request);
281     }
282
283     /**
284      * Returns the request's payload as a String.
285      *
286      * @param request
287      *            The request
288      * @return The data from the request's payload, as a string.
289      */

290     protected String getRequestPayload(SignableRequest<?> request) {
291         return newString(getBinaryRequestPayload(request));
292     }
293
294     /**
295      * Returns the request's payload contents as a String, without processing
296      * any query string params (i.e. no form encoding for query params).
297      *
298      * @param request
299      *            The request
300      * @return the request's payload contents as a String, not including any
301      *         form encoding of query string params.
302      */

303     protected String getRequestPayloadWithoutQueryParams(SignableRequest<?> request) {
304         return newString(getBinaryRequestPayloadWithoutQueryParams(request));
305     }
306
307     /**
308      * Returns the request's payload contents as binary data, without processing
309      * any query string params (i.e. no form encoding for query params).
310      *
311      * @param request
312      *            The request
313      * @return The request's payload contents as binary data, not including any
314      *         form encoding of query string params.
315      */

316     protected byte[] getBinaryRequestPayloadWithoutQueryParams(SignableRequest<?> request) {
317         InputStream content = getBinaryRequestPayloadStreamWithoutQueryParams(request);
318
319         try {
320             ReadLimitInfo info = request.getReadLimitInfo();
321             content.mark(info == null ? -1 : info.getReadLimit());
322             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
323             byte[] buffer = new byte[1024 * 5];
324             while (true) {
325                 int bytesRead = content.read(buffer);
326                 if (bytesRead == -1) break;
327
328                 byteArrayOutputStream.write(buffer, 0, bytesRead);
329             }
330
331             byteArrayOutputStream.close();
332             content.reset();
333
334             return byteArrayOutputStream.toByteArray();
335         } catch (Exception e) {
336             throw new SdkClientException("Unable to read request payload to sign request: " + e.getMessage(), e);
337         }
338     }
339
340     protected InputStream getBinaryRequestPayloadStream(SignableRequest<?> request) {
341         if (SdkHttpUtils.usePayloadForQueryParameters(request)) {
342             String encodedParameters = SdkHttpUtils.encodeParameters(request);
343             if (encodedParameters == null)
344                 return new ByteArrayInputStream(new byte[0]);
345
346             return new ByteArrayInputStream(
347                     encodedParameters.getBytes(UTF8));
348         }
349
350         return getBinaryRequestPayloadStreamWithoutQueryParams(request);
351     }
352
353     protected InputStream getBinaryRequestPayloadStreamWithoutQueryParams(SignableRequest<?> request) {
354         try {
355             InputStream is = request.getContentUnwrapped();
356             if (is == null)
357                 return new ByteArrayInputStream(new byte[0]);
358             if (!is.markSupported())
359                 throw new SdkClientException("Unable to read request payload to sign request.");
360             return is;
361         } catch (AmazonClientException e) {
362             throw e;
363         } catch (Exception e) {
364             throw new SdkClientException("Unable to read request payload to sign request: " + e.getMessage(), e);
365         }
366     }
367
368     protected String getCanonicalizedResourcePath(String resourcePath) {
369         return getCanonicalizedResourcePath(resourcePath, true);
370     }
371
372     protected String getCanonicalizedResourcePath(String resourcePath, boolean urlEncode) {
373         if (resourcePath == null || resourcePath.isEmpty()) {
374             return "/";
375         } else {
376             String value = urlEncode ? SdkHttpUtils.urlEncode(resourcePath, true) : resourcePath;
377             if (value.startsWith("/")) {
378                 return value;
379             } else {
380                 return "/".concat(value);
381             }
382         }
383     }
384
385     protected String getCanonicalizedEndpoint(URI endpoint) {
386         String endpointForStringToSign = StringUtils.lowerCase(endpoint.getHost());
387         /*
388          * Apache HttpClient will omit the port in the Host header for default
389          * port values (i.e. 80 for HTTP and 443 for HTTPS) even if we
390          * explicitly specify it, so we need to be careful that we use the same
391          * value here when we calculate the string to sign and in the Host
392          * header we send in the HTTP request.
393          */

394         if (SdkHttpUtils.isUsingNonDefaultPort(endpoint)) {
395             endpointForStringToSign += ":" + endpoint.getPort();
396         }
397
398         return endpointForStringToSign;
399     }
400
401     /**
402      * Loads the individual access key ID and secret key from the specified
403      * credentials, ensuring that access to the credentials is synchronized on
404      * the credentials object itself, and trimming any extra whitespace from the
405      * credentials.
406      * <p>
407      * Returns either a {@link BasicSessionCredentials} or a
408      * {@link BasicAWSCredentials} object, depending on the input type.
409      *
410      * @param credentials
411      * @return A new credentials object with the sanitized credentials.
412      */

413     protected AWSCredentials sanitizeCredentials(AWSCredentials credentials) {
414         String accessKeyId = null;
415         String secretKey   = null;
416         String token = null;
417         synchronized (credentials) {
418             accessKeyId = credentials.getAWSAccessKeyId();
419             secretKey   = credentials.getAWSSecretKey();
420             if ( credentials instanceof AWSSessionCredentials ) {
421                 token = ((AWSSessionCredentials) credentials).getSessionToken();
422             }
423         }
424         if (secretKey != null) secretKey = secretKey.trim();
425         if (accessKeyId != null) accessKeyId = accessKeyId.trim();
426         if (token != null) token = token.trim();
427
428         if (credentials instanceof AWSSessionCredentials) {
429             return new BasicSessionCredentials(accessKeyId, secretKey, token);
430         }
431
432         return new BasicAWSCredentials(accessKeyId, secretKey);
433     }
434
435     /**
436      * Safely converts a UTF-8 encoded byte array into a String.
437      *
438      * @param bytes UTF-8 encoded binary character data.
439      *
440      * @return The converted String object.
441      */

442     protected String newString(byte[] bytes) {
443         return new String(bytes, UTF8);
444     }
445
446     /**
447      * Returns the current time minus the given offset in seconds.
448      * The intent is to adjust the current time in the running JVM to the
449      * corresponding wall clock time at AWS for request signing purposes.
450      *
451      * @param offsetInSeconds
452      *            offset in seconds
453      */

454     protected Date getSignatureDate(int offsetInSeconds) {
455         return new Date(System.currentTimeMillis() - offsetInSeconds*1000);
456     }
457
458     /**
459      * Returns the time offset in seconds.
460      */

461     @Deprecated
462     protected int getTimeOffset(SignableRequest<?> request) {
463         final int globleOffset = SDKGlobalTime.getGlobalTimeOffset();
464         return globleOffset == 0 ? request.getTimeOffset() : globleOffset;
465     }
466
467     /**
468      * Adds session credentials to the request given.
469      *
470      * @param request
471      *            The request to add session credentials information to
472      * @param credentials
473      *            The session credentials to add to the request
474      */

475     protected abstract void addSessionCredentials(SignableRequest<?> request,
476             AWSSessionCredentials credentials);
477
478
479     /**
480      * Returns the re-usable thread local version of MessageDigest.
481      * @return
482      */

483     private static MessageDigest getMessageDigestInstance() {
484         MessageDigest messageDigest = SHA256_MESSAGE_DIGEST.get();
485         messageDigest.reset();
486         return messageDigest;
487     }
488
489 }
490