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