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.internal;
17
18 import java.io.ByteArrayInputStream;
19 import java.io.InputStream;
20 import java.nio.charset.StandardCharsets;
21 import java.security.DigestInputStream;
22 import java.security.MessageDigest;
23 import java.security.NoSuchAlgorithmException;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.SortedMap;
29 import java.util.TreeMap;
30 import javax.crypto.Mac;
31 import javax.crypto.spec.SecretKeySpec;
32 import software.amazon.awssdk.annotations.SdkInternalApi;
33 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
34 import software.amazon.awssdk.auth.credentials.AwsCredentials;
35 import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
36 import software.amazon.awssdk.core.exception.SdkClientException;
37 import software.amazon.awssdk.core.io.SdkDigestInputStream;
38 import software.amazon.awssdk.core.signer.Signer;
39 import software.amazon.awssdk.http.ContentStreamProvider;
40 import software.amazon.awssdk.http.SdkHttpFullRequest;
41 import software.amazon.awssdk.utils.BinaryUtils;
42 import software.amazon.awssdk.utils.StringUtils;
43 import software.amazon.awssdk.utils.http.SdkHttpUtils;
44
45 /**
46  * Abstract base class for AWS signing protocol implementations. Provides
47  * utilities commonly needed by signing protocols such as computing
48  * canonicalized host names, query string parameters, etc.
49  * <p>
50  * Not intended to be sub-classed by developers.
51  */

52 @SdkInternalApi
53 public abstract class AbstractAwsSigner implements Signer {
54
55     private static final ThreadLocal<MessageDigest> SHA256_MESSAGE_DIGEST;
56
57     static {
58         SHA256_MESSAGE_DIGEST = ThreadLocal.withInitial(() -> {
59             try {
60                 return MessageDigest.getInstance("SHA-256");
61             } catch (NoSuchAlgorithmException e) {
62                 throw SdkClientException.builder()
63                                         .message("Unable to get SHA256 Function" + e.getMessage())
64                                         .cause(e)
65                                         .build();
66             }
67         });
68     }
69
70     private static byte[] doHash(String text) throws SdkClientException {
71         try {
72             MessageDigest md = getMessageDigestInstance();
73             md.update(text.getBytes(StandardCharsets.UTF_8));
74             return md.digest();
75         } catch (Exception e) {
76             throw SdkClientException.builder()
77                                     .message("Unable to compute hash while signing request: " + e.getMessage())
78                                     .cause(e)
79                                     .build();
80         }
81     }
82
83     /**
84      * Returns the re-usable thread local version of MessageDigest.
85      */

86     private static MessageDigest getMessageDigestInstance() {
87         MessageDigest messageDigest = SHA256_MESSAGE_DIGEST.get();
88         messageDigest.reset();
89         return messageDigest;
90     }
91
92     /**
93      * Computes an RFC 2104-compliant HMAC signature and returns the result as a
94      * Base64 encoded string.
95      */

96     protected String signAndBase64Encode(String data, String key,
97                                          SigningAlgorithm algorithm) throws SdkClientException {
98         return signAndBase64Encode(data.getBytes(StandardCharsets.UTF_8), key, algorithm);
99     }
100
101     /**
102      * Computes an RFC 2104-compliant HMAC signature for an array of bytes and
103      * returns the result as a Base64 encoded string.
104      */

105     private String signAndBase64Encode(byte[] data, String key,
106                                        SigningAlgorithm algorithm) throws SdkClientException {
107         try {
108             byte[] signature = sign(data, key.getBytes(StandardCharsets.UTF_8), algorithm);
109             return BinaryUtils.toBase64(signature);
110         } catch (Exception e) {
111             throw SdkClientException.builder()
112                                     .message("Unable to calculate a request signature: " + e.getMessage())
113                                     .cause(e)
114                                     .build();
115         }
116     }
117
118     protected byte[] signWithMac(String stringData, Mac mac) {
119         try {
120             return mac.doFinal(stringData.getBytes(StandardCharsets.UTF_8));
121         } catch (Exception e) {
122             throw SdkClientException.builder()
123                                     .message("Unable to calculate a request signature: " + e.getMessage())
124                                     .cause(e)
125                                     .build();
126         }
127     }
128
129     protected byte[] sign(String stringData, byte[] key,
130                        SigningAlgorithm algorithm) throws SdkClientException {
131         try {
132             byte[] data = stringData.getBytes(StandardCharsets.UTF_8);
133             return sign(data, key, algorithm);
134         } catch (Exception e) {
135             throw SdkClientException.builder()
136                                     .message("Unable to calculate a request signature: " + e.getMessage())
137                                     .cause(e)
138                                     .build();
139         }
140     }
141
142     protected byte[] sign(byte[] data, byte[] key, SigningAlgorithm algorithm) throws SdkClientException {
143         try {
144             Mac mac = algorithm.getMac();
145             mac.init(new SecretKeySpec(key, algorithm.toString()));
146             return mac.doFinal(data);
147         } catch (Exception e) {
148             throw SdkClientException.builder()
149                                     .message("Unable to calculate a request signature: " + e.getMessage())
150                                     .cause(e)
151                                     .build();
152         }
153     }
154
155     /**
156      * Hashes the string contents (assumed to be UTF-8) using the SHA-256
157      * algorithm.
158      *
159      * @param text The string to hash.
160      * @return The hashed bytes from the specified string.
161      * @throws SdkClientException If the hash cannot be computed.
162      */

163     static byte[] hash(String text) throws SdkClientException {
164         return AbstractAwsSigner.doHash(text);
165     }
166
167     byte[] hash(InputStream input) throws SdkClientException {
168         try {
169             MessageDigest md = getMessageDigestInstance();
170             @SuppressWarnings("resource")
171             DigestInputStream digestInputStream = new SdkDigestInputStream(
172                     input, md);
173             byte[] buffer = new byte[1024];
174             while (digestInputStream.read(buffer) > -1) {
175                 ;
176             }
177             return digestInputStream.getMessageDigest().digest();
178         } catch (Exception e) {
179             throw SdkClientException.builder()
180                                     .message("Unable to compute hash while signing request: " + e.getMessage())
181                                     .cause(e)
182                                     .build();
183         }
184     }
185
186     /**
187      * Hashes the binary data using the SHA-256 algorithm.
188      *
189      * @param data The binary data to hash.
190      * @return The hashed bytes from the specified data.
191      * @throws SdkClientException If the hash cannot be computed.
192      */

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

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

225         for (Map.Entry<String, List<String>> entry : parameters.entrySet()) {
226             String encodedParamName = SdkHttpUtils.urlEncode(entry.getKey());
227             List<String> paramValues = entry.getValue();
228             List<String> encodedValues = new ArrayList<>(paramValues.size());
229             for (String value : paramValues) {
230                 String encodedValue = SdkHttpUtils.urlEncode(value);
231
232                 // Null values should be treated as empty for the purposes of signing, not missing.
233                 // For example "?foo=" instead of "?foo".
234                 String signatureFormattedEncodedValue = encodedValue == null ? "" : encodedValue;
235
236                 encodedValues.add(signatureFormattedEncodedValue);
237             }
238             Collections.sort(encodedValues);
239             sorted.put(encodedParamName, encodedValues);
240
241         }
242
243         return SdkHttpUtils.flattenQueryParameters(sorted).orElse("");
244     }
245
246     protected InputStream getBinaryRequestPayloadStream(ContentStreamProvider streamProvider) {
247         try {
248             if (streamProvider == null) {
249                 return new ByteArrayInputStream(new byte[0]);
250             }
251             return streamProvider.newStream();
252         } catch (SdkClientException e) {
253             throw e;
254         } catch (Exception e) {
255             throw SdkClientException.builder()
256                                     .message("Unable to read request payload to sign request: " + e.getMessage())
257                                     .cause(e)
258                                     .build();
259         }
260     }
261
262     String getCanonicalizedResourcePath(String resourcePath, boolean urlEncode) {
263         if (StringUtils.isEmpty(resourcePath)) {
264             return "/";
265         } else {
266             String value = urlEncode ? SdkHttpUtils.urlEncodeIgnoreSlashes(resourcePath) : resourcePath;
267             if (value.startsWith("/")) {
268                 return value;
269             } else {
270                 return "/".concat(value);
271             }
272         }
273     }
274
275     protected String getCanonicalizedEndpoint(SdkHttpFullRequest request) {
276         String endpointForStringToSign = StringUtils.lowerCase(request.host());
277
278         // Omit the port from the endpoint if we're using the default port for the protocol. Some HTTP clients (ie. Apache) don't
279         // allow you to specify it in the request, so we're standardizing around not including it. See SdkHttpRequest#port().
280         if (!SdkHttpUtils.isUsingStandardPort(request.protocol(), request.port())) {
281             endpointForStringToSign += ":" + request.port();
282         }
283
284         return endpointForStringToSign;
285     }
286
287     /**
288      * Loads the individual access key ID and secret key from the specified credentials, trimming any extra whitespace from the
289      * credentials.
290      *
291      * <p>Returns either a {@link AwsSessionCredentials} or a {@link AwsBasicCredentials} object, depending on the input type.
292      *
293      * @return A new credentials object with the sanitized credentials.
294      */

295     protected AwsCredentials sanitizeCredentials(AwsCredentials credentials) {
296         String accessKeyId = StringUtils.trim(credentials.accessKeyId());
297         String secretKey = StringUtils.trim(credentials.secretAccessKey());
298
299         if (credentials instanceof AwsSessionCredentials) {
300             AwsSessionCredentials sessionCredentials = (AwsSessionCredentials) credentials;
301             return AwsSessionCredentials.create(accessKeyId,
302                                                 secretKey,
303                                                 StringUtils.trim(sessionCredentials.sessionToken()));
304         }
305
306         return AwsBasicCredentials.create(accessKeyId, secretKey);
307     }
308
309     /**
310      * Adds session credentials to the request given.
311      *
312      * @param mutableRequest The request to add session credentials information to
313      * @param credentials    The session credentials to add to the request
314      */

315     protected abstract void addSessionCredentials(SdkHttpFullRequest.Builder mutableRequest,
316                                                   AwsSessionCredentials credentials);
317
318 }
319