1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 package org.eclipse.jgit.lfs.server.s3;
45
46 import static java.nio.charset.StandardCharsets.UTF_8;
47 import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
48
49 import java.io.UnsupportedEncodingException;
50 import java.net.URL;
51 import java.net.URLEncoder;
52 import java.security.MessageDigest;
53 import java.text.MessageFormat;
54 import java.text.SimpleDateFormat;
55 import java.util.ArrayList;
56 import java.util.Collections;
57 import java.util.Date;
58 import java.util.Iterator;
59 import java.util.List;
60 import java.util.Locale;
61 import java.util.Map;
62 import java.util.SimpleTimeZone;
63 import java.util.SortedMap;
64 import java.util.TreeMap;
65
66 import javax.crypto.Mac;
67 import javax.crypto.spec.SecretKeySpec;
68
69 import org.eclipse.jgit.lfs.lib.Constants;
70 import org.eclipse.jgit.lfs.server.internal.LfsServerText;
71
72
73
74
75
76
77
78 class SignerV4 {
79 static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
80
81 private static final String ALGORITHM = "HMAC-SHA256";
82 private static final String DATE_STRING_FORMAT = "yyyyMMdd";
83 private static final String HEX = "0123456789abcdef";
84 private static final String HMACSHA256 = "HmacSHA256";
85 private static final String ISO8601_BASIC_FORMAT = "yyyyMMdd'T'HHmmss'Z'";
86 private static final String S3 = "s3";
87 private static final String SCHEME = "AWS4";
88 private static final String TERMINATOR = "aws4_request";
89 private static final String UTC = "UTC";
90 private static final String X_AMZ_ALGORITHM = "X-Amz-Algorithm";
91 private static final String X_AMZ_CREDENTIAL = "X-Amz-Credential";
92 private static final String X_AMZ_DATE = "X-Amz-Date";
93 private static final String X_AMZ_SIGNATURE = "X-Amz-Signature";
94 private static final String X_AMZ_SIGNED_HEADERS = "X-Amz-SignedHeaders";
95
96 static final String X_AMZ_CONTENT_SHA256 = "x-amz-content-sha256";
97 static final String X_AMZ_EXPIRES = "X-Amz-Expires";
98 static final String X_AMZ_STORAGE_CLASS = "x-amz-storage-class";
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125 static String createAuthorizationQuery(S3Config bucketConfig, URL url,
126 String httpMethod, Map<String, String> headers,
127 Map<String, String> queryParameters, String bodyHash) {
128 addHostHeader(url, headers);
129
130 queryParameters.put(X_AMZ_ALGORITHM, SCHEME + "-" + ALGORITHM);
131
132 Date now = new Date();
133 String dateStamp = dateStamp(now);
134 String scope = scope(bucketConfig.getRegion(), dateStamp);
135 queryParameters.put(X_AMZ_CREDENTIAL,
136 bucketConfig.getAccessKey() + "/" + scope);
137
138 String dateTimeStampISO8601 = dateTimeStampISO8601(now);
139 queryParameters.put(X_AMZ_DATE, dateTimeStampISO8601);
140
141 String canonicalizedHeaderNames = canonicalizeHeaderNames(headers);
142 queryParameters.put(X_AMZ_SIGNED_HEADERS, canonicalizedHeaderNames);
143
144 String canonicalizedQueryParameters = canonicalizeQueryString(
145 queryParameters);
146 String canonicalizedHeaders = canonicalizeHeaderString(headers);
147 String canonicalRequest = canonicalRequest(url, httpMethod,
148 canonicalizedQueryParameters, canonicalizedHeaderNames,
149 canonicalizedHeaders, bodyHash);
150 byte[] signature = createSignature(bucketConfig, dateTimeStampISO8601,
151 dateStamp, scope, canonicalRequest);
152 queryParameters.put(X_AMZ_SIGNATURE, toHex(signature));
153
154 return formatAuthorizationQuery(queryParameters);
155 }
156
157 private static String formatAuthorizationQuery(
158 Map<String, String> queryParameters) {
159 StringBuilder s = new StringBuilder();
160 for (String key : queryParameters.keySet()) {
161 appendQuery(s, key, queryParameters.get(key));
162 }
163 return s.toString();
164 }
165
166 private static void appendQuery(StringBuilder s, String key,
167 String value) {
168 if (s.length() != 0) {
169 s.append("&");
170 }
171 s.append(key).append("=").append(value);
172 }
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194 static Map<String, String> createHeaderAuthorization(
195 S3Config bucketConfig, URL url, String httpMethod,
196 Map<String, String> headers, String bodyHash) {
197 addHostHeader(url, headers);
198
199 Date now = new Date();
200 String dateTimeStamp = dateTimeStampISO8601(now);
201 headers.put(X_AMZ_DATE, dateTimeStamp);
202
203 String canonicalizedHeaderNames = canonicalizeHeaderNames(headers);
204 String canonicalizedHeaders = canonicalizeHeaderString(headers);
205 String canonicalRequest = canonicalRequest(url, httpMethod, "",
206 canonicalizedHeaderNames, canonicalizedHeaders, bodyHash);
207 String dateStamp = dateStamp(now);
208 String scope = scope(bucketConfig.getRegion(), dateStamp);
209
210 byte[] signature = createSignature(bucketConfig, dateTimeStamp,
211 dateStamp, scope, canonicalRequest);
212
213 headers.put(HDR_AUTHORIZATION, formatAuthorizationHeader(bucketConfig,
214 canonicalizedHeaderNames, scope, signature));
215
216 return headers;
217 }
218
219 private static String formatAuthorizationHeader(
220 S3Config bucketConfig, String canonicalizedHeaderNames,
221 String scope, byte[] signature) {
222 StringBuilder s = new StringBuilder();
223 s.append(SCHEME).append("-").append(ALGORITHM).append(" ");
224 s.append("Credential=").append(bucketConfig.getAccessKey()).append("/")
225 .append(scope).append(",");
226 s.append("SignedHeaders=").append(canonicalizedHeaderNames).append(",");
227 s.append("Signature=").append(toHex(signature));
228 return s.toString();
229 }
230
231 private static void addHostHeader(URL url,
232 Map<String, String> headers) {
233 StringBuilder hostHeader = new StringBuilder(url.getHost());
234 int port = url.getPort();
235 if (port > -1) {
236 hostHeader.append(":").append(port);
237 }
238 headers.put("Host", hostHeader.toString());
239 }
240
241 private static String canonicalizeHeaderNames(
242 Map<String, String> headers) {
243 List<String> sortedHeaders = new ArrayList<>();
244 sortedHeaders.addAll(headers.keySet());
245 Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
246
247 StringBuilder buffer = new StringBuilder();
248 for (String header : sortedHeaders) {
249 if (buffer.length() > 0)
250 buffer.append(";");
251 buffer.append(header.toLowerCase(Locale.ROOT));
252 }
253
254 return buffer.toString();
255 }
256
257 private static String canonicalizeHeaderString(
258 Map<String, String> headers) {
259 if (headers == null || headers.isEmpty()) {
260 return "";
261 }
262
263 List<String> sortedHeaders = new ArrayList<>();
264 sortedHeaders.addAll(headers.keySet());
265 Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
266
267 StringBuilder buffer = new StringBuilder();
268 for (String key : sortedHeaders) {
269 buffer.append(
270 key.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ") + ":"
271 + headers.get(key).replaceAll("\\s+", " "));
272 buffer.append("\n");
273 }
274
275 return buffer.toString();
276 }
277
278 private static String dateStamp(Date now) {
279
280 SimpleDateFormat dateStampFormat = new SimpleDateFormat(
281 DATE_STRING_FORMAT);
282 dateStampFormat.setTimeZone(new SimpleTimeZone(0, UTC));
283 String dateStamp = dateStampFormat.format(now);
284 return dateStamp;
285 }
286
287 private static String dateTimeStampISO8601(Date now) {
288
289 SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
290 ISO8601_BASIC_FORMAT);
291 dateTimeFormat.setTimeZone(new SimpleTimeZone(0, UTC));
292 String dateTimeStamp = dateTimeFormat.format(now);
293 return dateTimeStamp;
294 }
295
296 private static String scope(String region, String dateStamp) {
297 String scope = String.format("%s/%s/%s/%s", dateStamp, region, S3,
298 TERMINATOR);
299 return scope;
300 }
301
302 private static String canonicalizeQueryString(
303 Map<String, String> parameters) {
304 if (parameters == null || parameters.isEmpty()) {
305 return "";
306 }
307
308 SortedMap<String, String> sorted = new TreeMap<>();
309
310 Iterator<Map.Entry<String, String>> pairs = parameters.entrySet()
311 .iterator();
312 while (pairs.hasNext()) {
313 Map.Entry<String, String> pair = pairs.next();
314 String key = pair.getKey();
315 String value = pair.getValue();
316 sorted.put(urlEncode(key, false), urlEncode(value, false));
317 }
318
319 StringBuilder builder = new StringBuilder();
320 pairs = sorted.entrySet().iterator();
321 while (pairs.hasNext()) {
322 Map.Entry<String, String> pair = pairs.next();
323 builder.append(pair.getKey());
324 builder.append("=");
325 builder.append(pair.getValue());
326 if (pairs.hasNext()) {
327 builder.append("&");
328 }
329 }
330
331 return builder.toString();
332 }
333
334 private static String canonicalRequest(URL endpoint, String httpMethod,
335 String queryParameters, String canonicalizedHeaderNames,
336 String canonicalizedHeaders, String bodyHash) {
337 return String.format("%s\n%s\n%s\n%s\n%s\n%s",
338 httpMethod, canonicalizeResourcePath(endpoint),
339 queryParameters, canonicalizedHeaders, canonicalizedHeaderNames,
340 bodyHash);
341 }
342
343 private static String canonicalizeResourcePath(URL endpoint) {
344 if (endpoint == null) {
345 return "/";
346 }
347 String path = endpoint.getPath();
348 if (path == null || path.isEmpty()) {
349 return "/";
350 }
351
352 String encodedPath = urlEncode(path, true);
353 if (encodedPath.startsWith("/")) {
354 return encodedPath;
355 } else {
356 return "/" + encodedPath;
357 }
358 }
359
360 private static byte[] hash(String s) {
361 MessageDigest md = Constants.newMessageDigest();
362 md.update(s.getBytes(UTF_8));
363 return md.digest();
364 }
365
366 private static byte[] sign(String stringData, byte[] key) {
367 try {
368 byte[] data = stringData.getBytes(UTF_8);
369 Mac mac = Mac.getInstance(HMACSHA256);
370 mac.init(new SecretKeySpec(key, HMACSHA256));
371 return mac.doFinal(data);
372 } catch (Exception e) {
373 throw new RuntimeException(MessageFormat.format(
374 LfsServerText.get().failedToCalcSignature, e.getMessage()),
375 e);
376 }
377 }
378
379 private static String stringToSign(String scheme, String algorithm,
380 String dateTime, String scope, String canonicalRequest) {
381 return String.format("%s-%s\n%s\n%s\n%s",
382 scheme, algorithm, dateTime, scope,
383 toHex(hash(canonicalRequest)));
384 }
385
386 private static String toHex(byte[] bytes) {
387 StringBuilder builder = new StringBuilder(2 * bytes.length);
388 for (byte b : bytes) {
389 builder.append(HEX.charAt((b & 0xF0) >> 4));
390 builder.append(HEX.charAt(b & 0xF));
391 }
392 return builder.toString();
393 }
394
395 private static String urlEncode(String url, boolean keepPathSlash) {
396 String encoded;
397 try {
398 encoded = URLEncoder.encode(url, UTF_8.name());
399 } catch (UnsupportedEncodingException e) {
400 throw new RuntimeException(LfsServerText.get().unsupportedUtf8, e);
401 }
402 if (keepPathSlash) {
403 encoded = encoded.replace("%2F", "/");
404 }
405 return encoded;
406 }
407
408 private static byte[] createSignature(S3Config bucketConfig,
409 String dateTimeStamp, String dateStamp,
410 String scope, String canonicalRequest) {
411 String stringToSign = stringToSign(SCHEME, ALGORITHM, dateTimeStamp,
412 scope, canonicalRequest);
413
414 byte[] signature = (SCHEME + bucketConfig.getSecretKey())
415 .getBytes(UTF_8);
416 signature = sign(dateStamp, signature);
417 signature = sign(bucketConfig.getRegion(), signature);
418 signature = sign(S3, signature);
419 signature = sign(TERMINATOR, signature);
420 signature = sign(stringToSign, signature);
421 return signature;
422 }
423 }