1 package com.github.davidmoten.aws.lw.client.internal.auth;
2
3 import java.net.URL;
4 import java.nio.charset.StandardCharsets;
5 import java.security.InvalidKeyException;
6 import java.security.NoSuchAlgorithmException;
7 import java.text.SimpleDateFormat;
8 import java.util.ArrayList;
9 import java.util.Collections;
10 import java.util.Date;
11 import java.util.List;
12 import java.util.Locale;
13 import java.util.Map;
14 import java.util.Map.Entry;
15 import java.util.Optional;
16 import java.util.SimpleTimeZone;
17 import java.util.SortedMap;
18 import java.util.TreeMap;
19 import java.util.stream.Collectors;
20
21 import javax.crypto.Mac;
22 import javax.crypto.spec.SecretKeySpec;
23
24 import com.github.davidmoten.aws.lw.client.internal.Clock;
25 import com.github.davidmoten.aws.lw.client.internal.util.Preconditions;
26 import com.github.davidmoten.aws.lw.client.internal.util.Util;
27
28
29
30
31 public final class AwsSignatureVersion4 {
32
33 static final String ALGORITHM_HMAC_SHA256 = "HmacSHA256";
34
35 public static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
36 public static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
37
38 public static final String SCHEME = "AWS4";
39 public static final String ALGORITHM = "HMAC-SHA256";
40 public static final String TERMINATOR = "aws4_request";
41
42
43 private static final String ISO8601BasicFormat = "yyyyMMdd'T'HHmmss'Z'";
44 private static final String DateStringFormat = "yyyyMMdd";
45
46 private AwsSignatureVersion4() {
47
48 }
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74 public static String computeSignatureForQueryAuth(URL endpointUrl, String httpMethod,
75 String serviceName, Optional<String> regionName, Clock clock, Map<String, String> headers,
76 Map<String, String> queryParameters, String bodyHash, String awsAccessKey,
77 String awsSecretKey, Optional<String> sessionToken) {
78
79
80 Date now = new Date(clock.time());
81 String dateTimeStamp = dateTimeFormat().format(now);
82
83
84 String hostHeader = endpointUrl.getHost();
85 int port = endpointUrl.getPort();
86 if (port > -1) {
87 hostHeader = hostHeader.concat(":" + port);
88 }
89 headers.put("Host", hostHeader);
90
91
92
93 String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers);
94 String canonicalizedHeaders = getCanonicalizedHeaderString(headers);
95
96
97 String dateStamp = dateStampFormat().format(now);
98 String scope = dateStamp + "/" + regionName.orElse("us-east-1") + "/" + serviceName + "/" + TERMINATOR;
99
100
101 queryParameters.put("X-Amz-Algorithm", SCHEME + "-" + ALGORITHM);
102 queryParameters.put("X-Amz-Credential", awsAccessKey + "/" + scope);
103
104
105
106 queryParameters.put("X-Amz-Date", dateTimeStamp);
107
108 queryParameters.put("X-Amz-SignedHeaders", canonicalizedHeaderNames);
109
110 if (sessionToken.isPresent()) {
111 queryParameters.put("X-Amz-Security-Token", sessionToken.get());
112 }
113
114
115
116 String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters);
117
118
119 String canonicalRequest = getCanonicalRequest(endpointUrl, httpMethod,
120 canonicalizedQueryParameters, canonicalizedHeaderNames, canonicalizedHeaders,
121 bodyHash);
122
123
124 String stringToSign = getStringToSign(SCHEME, ALGORITHM, dateTimeStamp, scope,
125 canonicalRequest);
126
127
128
129
130
131 byte[] kSecret = (SCHEME + awsSecretKey).getBytes(StandardCharsets.UTF_8);
132 byte[] kDate = sign(dateStamp, kSecret);
133 byte[] kRegion = sign(regionName.orElse("us-east-1"), kDate);
134 byte[] kService = sign(serviceName, kRegion);
135 byte[] kSigning = sign(TERMINATOR, kService);
136 byte[] signature = sign(stringToSign, kSigning);
137
138
139
140 StringBuilder authString = new StringBuilder();
141
142 authString.append("X-Amz-Algorithm=" + queryParameters.get("X-Amz-Algorithm"));
143 authString.append("&X-Amz-Credential=" + queryParameters.get("X-Amz-Credential"));
144 authString.append("&X-Amz-Date=" + queryParameters.get("X-Amz-Date"));
145 authString.append("&X-Amz-Expires=" + queryParameters.get("X-Amz-Expires"));
146 authString.append("&X-Amz-SignedHeaders=" + queryParameters.get("X-Amz-SignedHeaders"));
147 authString.append("&X-Amz-Signature=" + Util.toHex(signature));
148 if (sessionToken.isPresent()) {
149 authString.append("&X-Amz-Security-Token=" + Util.urlEncode(sessionToken.get(), false));
150 }
151 return authString.toString();
152 }
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177 public static String computeSignatureForAuthorizationHeader(URL endpointUrl, String httpMethod,
178 String serviceName, String regionName, Clock clock, Map<String, String> headers,
179 Map<String, String> queryParameters, String bodyHash, String awsAccessKey,
180 String awsSecretKey) {
181 Preconditions.checkNotNull(headers);
182 Preconditions.checkNotNull(queryParameters);
183 SimpleDateFormat dateTimeFormat = dateTimeFormat();
184 SimpleDateFormat dateStampFormat = dateStampFormat();
185
186
187 Date now = new Date(clock.time());
188 String dateTimeStamp = dateTimeFormat.format(now);
189
190
191 headers.put("x-amz-date", dateTimeStamp);
192
193 String hostHeader = endpointUrl.getHost();
194 int port = endpointUrl.getPort();
195 if (port > -1) {
196 hostHeader = hostHeader.concat(":" + port);
197 }
198 headers.put("Host", hostHeader);
199
200
201
202 String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers);
203 String canonicalizedHeaders = getCanonicalizedHeaderString(headers);
204
205
206 String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters);
207
208
209
210
211 String canonicalRequest = getCanonicalRequest(endpointUrl, httpMethod,
212 canonicalizedQueryParameters, canonicalizedHeaderNames, canonicalizedHeaders,
213 bodyHash);
214
215
216
217
218
219 String dateStamp = dateStampFormat.format(now);
220 String scope = dateStamp + "/" + regionName + "/" + serviceName + "/" + TERMINATOR;
221 String stringToSign = getStringToSign(SCHEME, ALGORITHM, dateTimeStamp, scope,
222 canonicalRequest);
223
224
225
226
227
228 byte[] kSecret = (SCHEME + awsSecretKey).getBytes(StandardCharsets.UTF_8);
229 byte[] kDate = sign(dateStamp, kSecret);
230 byte[] kRegion = sign(regionName, kDate);
231 byte[] kService = sign(serviceName, kRegion);
232 byte[] kSigning = sign(TERMINATOR, kService);
233 byte[] signature = sign(stringToSign, kSigning);
234
235 String credentialsAuthorizationHeader = "Credential=" + awsAccessKey + "/" + scope;
236 String signedHeadersAuthorizationHeader = "SignedHeaders=" + canonicalizedHeaderNames;
237 String signatureAuthorizationHeader = "Signature=" + Util.toHex(signature);
238
239 String authorizationHeader = SCHEME + "-" + ALGORITHM + " " + credentialsAuthorizationHeader
240 + ", " + signedHeadersAuthorizationHeader + ", " + signatureAuthorizationHeader;
241 return authorizationHeader;
242 }
243
244 static SimpleDateFormat dateTimeFormat() {
245 SimpleDateFormat sdf = new SimpleDateFormat(ISO8601BasicFormat);
246 sdf.setTimeZone(new SimpleTimeZone(0, "UTC"));
247 return sdf;
248 }
249
250 static SimpleDateFormat dateStampFormat() {
251 SimpleDateFormat sdf = new SimpleDateFormat(DateStringFormat);
252 sdf.setTimeZone(new SimpleTimeZone(0, "UTC"));
253 return sdf;
254 }
255
256
257
258
259
260
261
262
263
264 static String getCanonicalizeHeaderNames(Map<String, String> headers) {
265 List<String> sortedHeaders = new ArrayList<String>();
266 sortedHeaders.addAll(headers.keySet());
267 Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
268
269 StringBuilder buffer = new StringBuilder();
270 for (String header : sortedHeaders) {
271 if (buffer.length() > 0)
272 buffer.append(";");
273 buffer.append(header.toLowerCase(Locale.ENGLISH));
274 }
275
276 return buffer.toString();
277 }
278
279
280
281
282
283
284
285
286 static String getCanonicalizedHeaderString(Map<String, String> headers) {
287
288
289 List<String> sortedHeaders = new ArrayList<String>();
290 sortedHeaders.addAll(headers.keySet());
291 Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
292
293
294
295
296 StringBuilder buffer = new StringBuilder();
297 for (String key : sortedHeaders) {
298 buffer.append(key.toLowerCase(Locale.ENGLISH).replaceAll("\\s+", " ") + ":"
299 + headers.get(key).replaceAll("\\s+", " "));
300 buffer.append("\n");
301 }
302
303 return buffer.toString();
304 }
305
306
307
308
309
310
311
312
313
314
315
316
317
318 static String getCanonicalRequest(URL endpoint, String httpMethod,
319 String canonicalQueryParameters, String canonicalizedHeaderNames,
320 String canonicalizedHeaders, String bodyHash) {
321 return httpMethod + "\n"
322 + getCanonicalizedResourcePath(endpoint) + "\n"
323 + canonicalQueryParameters + "\n"
324 + canonicalizedHeaders + "\n"
325 + canonicalizedHeaderNames + "\n"
326 + bodyHash;
327 }
328
329
330
331
332
333
334
335 static String getCanonicalizedResourcePath(URL endpoint) {
336 Preconditions.checkNotNull(endpoint);
337 String path = endpoint.getPath();
338 if (path.isEmpty()) {
339 return "/";
340 } else {
341 return Util.urlEncode(path, true);
342 }
343 }
344
345
346
347
348
349
350
351
352
353
354
355
356
357 static String getCanonicalizedQueryString(Map<String, String> parameters) {
358 SortedMap<String, String> sorted = new TreeMap<String, String>();
359
360 for (Entry<String, String> pair : parameters.entrySet()) {
361 sorted.put(Util.urlEncode(pair.getKey(), false),
362 pair.getValue() == null ? null : Util.urlEncode(pair.getValue(), false));
363 }
364
365 return sorted
366 .entrySet()
367 .stream()
368 .map(pair -> pair.getKey() + "=" + blankIfNull(pair.getValue()))
369 .collect(Collectors.joining("&"));
370 }
371
372 private static String blankIfNull(String s) {
373 return s == null ? "" : s;
374 }
375
376 static String getStringToSign(String scheme, String algorithm, String dateTime, String scope,
377 String canonicalRequest) {
378 return scheme + "-" + algorithm + "\n" + dateTime + "\n" + scope + "\n"
379 + Util.toHex(Util.sha256(canonicalRequest));
380 }
381
382 static byte[] sign(String stringData, byte[] key) {
383 return sign(stringData, key, ALGORITHM_HMAC_SHA256);
384 }
385
386
387 static byte[] sign(String stringData, byte[] key, String algorithm) {
388 try {
389 byte[] data = stringData.getBytes(StandardCharsets.UTF_8);
390 Mac mac = Mac.getInstance(algorithm);
391 mac.init(new SecretKeySpec(key, algorithm));
392 return mac.doFinal(data);
393 } catch (NoSuchAlgorithmException | InvalidKeyException e) {
394 throw new RuntimeException(e);
395 }
396 }
397
398 }