| /* |
| * Copyright (C) 2015, Matthias Sohn <matthias.sohn@sap.com> |
| * Copyright (C) 2015, Sasa Zivkov <sasa.zivkov@sap.com> |
| * and other copyright owners as documented in the project's IP log. |
| * |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Distribution License v1.0 which |
| * accompanies this distribution, is reproduced below, and is |
| * available at http://www.eclipse.org/org/documents/edl-v10.php |
| * |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or |
| * without modification, are permitted provided that the following |
| * conditions are met: |
| * |
| * - Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * |
| * - Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following |
| * disclaimer in the documentation and/or other materials provided |
| * with the distribution. |
| * |
| * - Neither the name of the Eclipse Foundation, Inc. nor the |
| * names of its contributors may be used to endorse or promote |
| * products derived from this software without specific prior |
| * written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
| * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
| * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
| * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
| * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| package org.eclipse.jgit.lfs.server.s3; |
| |
| import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.net.URL; |
| import java.net.URLEncoder; |
| import java.nio.charset.StandardCharsets; |
| import java.security.MessageDigest; |
| import java.text.MessageFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.SimpleTimeZone; |
| import java.util.SortedMap; |
| import java.util.TreeMap; |
| |
| import javax.crypto.Mac; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| import org.eclipse.jgit.lfs.lib.Constants; |
| import org.eclipse.jgit.lfs.server.internal.LfsServerText; |
| |
| /** |
| * Signing support for Amazon AWS signing V4 |
| * <p> |
| * See |
| * http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html |
| */ |
| class SignerV4 { |
| static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"; //$NON-NLS-1$ |
| |
| private static final String ALGORITHM = "HMAC-SHA256"; //$NON-NLS-1$ |
| private static final String DATE_STRING_FORMAT = "yyyyMMdd"; //$NON-NLS-1$ |
| private static final String HEX = "0123456789abcdef"; //$NON-NLS-1$ |
| private static final String HMACSHA256 = "HmacSHA256"; //$NON-NLS-1$ |
| private static final String ISO8601_BASIC_FORMAT = "yyyyMMdd'T'HHmmss'Z'"; //$NON-NLS-1$ |
| private static final String S3 = "s3"; //$NON-NLS-1$ |
| private static final String SCHEME = "AWS4"; //$NON-NLS-1$ |
| private static final String TERMINATOR = "aws4_request"; //$NON-NLS-1$ |
| private static final String UTC = "UTC"; //$NON-NLS-1$ |
| private static final String X_AMZ_ALGORITHM = "X-Amz-Algorithm"; //$NON-NLS-1$ |
| private static final String X_AMZ_CREDENTIAL = "X-Amz-Credential"; //$NON-NLS-1$ |
| private static final String X_AMZ_DATE = "X-Amz-Date"; //$NON-NLS-1$ |
| private static final String X_AMZ_SIGNATURE = "X-Amz-Signature"; //$NON-NLS-1$ |
| private static final String X_AMZ_SIGNED_HEADERS = "X-Amz-SignedHeaders"; //$NON-NLS-1$ |
| |
| static final String X_AMZ_CONTENT_SHA256 = "x-amz-content-sha256"; //$NON-NLS-1$ |
| static final String X_AMZ_EXPIRES = "X-Amz-Expires"; //$NON-NLS-1$ |
| static final String X_AMZ_STORAGE_CLASS = "x-amz-storage-class"; //$NON-NLS-1$ |
| |
| /** |
| * Create an AWSV4 authorization for a request, suitable for embedding in |
| * query parameters. |
| * |
| * @param bucketConfig |
| * configuration of S3 storage bucket this request should be |
| * signed for |
| * @param url |
| * HTTP request URL |
| * @param httpMethod |
| * HTTP method |
| * @param headers |
| * The HTTP request headers; 'Host' and 'X-Amz-Date' will be |
| * added to this set. |
| * @param queryParameters |
| * Any query parameters that will be added to the endpoint. The |
| * parameters should be specified in canonical format. |
| * @param bodyHash |
| * Pre-computed SHA256 hash of the request body content; this |
| * value should also be set as the header 'X-Amz-Content-SHA256' |
| * for non-streaming uploads. |
| * @return The computed authorization string for the request. This value |
| * needs to be set as the header 'Authorization' on the subsequent |
| * HTTP request. |
| */ |
| static String createAuthorizationQuery(S3Config bucketConfig, URL url, |
| String httpMethod, Map<String, String> headers, |
| Map<String, String> queryParameters, String bodyHash) { |
| addHostHeader(url, headers); |
| |
| queryParameters.put(X_AMZ_ALGORITHM, SCHEME + "-" + ALGORITHM); //$NON-NLS-1$ |
| |
| Date now = new Date(); |
| String dateStamp = dateStamp(now); |
| String scope = scope(bucketConfig.getRegion(), dateStamp); |
| queryParameters.put(X_AMZ_CREDENTIAL, |
| bucketConfig.getAccessKey() + "/" + scope); //$NON-NLS-1$ |
| |
| String dateTimeStampISO8601 = dateTimeStampISO8601(now); |
| queryParameters.put(X_AMZ_DATE, dateTimeStampISO8601); |
| |
| String canonicalizedHeaderNames = canonicalizeHeaderNames(headers); |
| queryParameters.put(X_AMZ_SIGNED_HEADERS, canonicalizedHeaderNames); |
| |
| String canonicalizedQueryParameters = canonicalizeQueryString( |
| queryParameters); |
| String canonicalizedHeaders = canonicalizeHeaderString(headers); |
| String canonicalRequest = canonicalRequest(url, httpMethod, |
| canonicalizedQueryParameters, canonicalizedHeaderNames, |
| canonicalizedHeaders, bodyHash); |
| byte[] signature = createSignature(bucketConfig, dateTimeStampISO8601, |
| dateStamp, scope, canonicalRequest); |
| queryParameters.put(X_AMZ_SIGNATURE, toHex(signature)); |
| |
| return formatAuthorizationQuery(queryParameters); |
| } |
| |
| private static String formatAuthorizationQuery( |
| Map<String, String> queryParameters) { |
| StringBuilder s = new StringBuilder(); |
| for (String key : queryParameters.keySet()) { |
| appendQuery(s, key, queryParameters.get(key)); |
| } |
| return s.toString(); |
| } |
| |
| private static void appendQuery(StringBuilder s, String key, |
| String value) { |
| if (s.length() != 0) { |
| s.append("&"); //$NON-NLS-1$ |
| } |
| s.append(key).append("=").append(value); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Sign headers for given bucket, url and HTTP method and add signature in |
| * Authorization header. |
| * |
| * @param bucketConfig |
| * configuration of S3 storage bucket this request should be |
| * signed for |
| * @param url |
| * HTTP request URL |
| * @param httpMethod |
| * HTTP method |
| * @param headers |
| * HTTP headers to sign |
| * @param bodyHash |
| * Pre-computed SHA256 hash of the request body content; this |
| * value should also be set as the header 'X-Amz-Content-SHA256' |
| * for non-streaming uploads. |
| * @return HTTP headers signd by an Authorization header added to the |
| * headers |
| */ |
| static Map<String, String> createHeaderAuthorization( |
| S3Config bucketConfig, URL url, String httpMethod, |
| Map<String, String> headers, String bodyHash) { |
| addHostHeader(url, headers); |
| |
| Date now = new Date(); |
| String dateTimeStamp = dateTimeStampISO8601(now); |
| headers.put(X_AMZ_DATE, dateTimeStamp); |
| |
| String canonicalizedHeaderNames = canonicalizeHeaderNames(headers); |
| String canonicalizedHeaders = canonicalizeHeaderString(headers); |
| String canonicalRequest = canonicalRequest(url, httpMethod, "", //$NON-NLS-1$ |
| canonicalizedHeaderNames, canonicalizedHeaders, bodyHash); |
| String dateStamp = dateStamp(now); |
| String scope = scope(bucketConfig.getRegion(), dateStamp); |
| |
| byte[] signature = createSignature(bucketConfig, dateTimeStamp, |
| dateStamp, scope, canonicalRequest); |
| |
| headers.put(HDR_AUTHORIZATION, formatAuthorizationHeader(bucketConfig, |
| canonicalizedHeaderNames, scope, signature)); // $NON-NLS-1$ |
| |
| return headers; |
| } |
| |
| private static String formatAuthorizationHeader( |
| S3Config bucketConfig, String canonicalizedHeaderNames, |
| String scope, byte[] signature) { |
| StringBuilder s = new StringBuilder(); |
| s.append(SCHEME).append("-").append(ALGORITHM).append(" "); //$NON-NLS-1$ //$NON-NLS-2$ |
| s.append("Credential=").append(bucketConfig.getAccessKey()).append("/") //$NON-NLS-1$//$NON-NLS-2$ |
| .append(scope).append(","); //$NON-NLS-1$ |
| s.append("SignedHeaders=").append(canonicalizedHeaderNames).append(","); //$NON-NLS-1$ //$NON-NLS-2$ |
| s.append("Signature=").append(toHex(signature)); //$NON-NLS-1$ |
| return s.toString(); |
| } |
| |
| private static void addHostHeader(URL url, |
| Map<String, String> headers) { |
| StringBuilder hostHeader = new StringBuilder(url.getHost()); |
| int port = url.getPort(); |
| if (port > -1) { |
| hostHeader.append(":").append(port); //$NON-NLS-1$ |
| } |
| headers.put("Host", hostHeader.toString()); //$NON-NLS-1$ |
| } |
| |
| private static String canonicalizeHeaderNames( |
| Map<String, String> headers) { |
| List<String> sortedHeaders = new ArrayList<>(); |
| sortedHeaders.addAll(headers.keySet()); |
| Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER); |
| |
| StringBuilder buffer = new StringBuilder(); |
| for (String header : sortedHeaders) { |
| if (buffer.length() > 0) |
| buffer.append(";"); //$NON-NLS-1$ |
| buffer.append(header.toLowerCase(Locale.ROOT)); |
| } |
| |
| return buffer.toString(); |
| } |
| |
| private static String canonicalizeHeaderString( |
| Map<String, String> headers) { |
| if (headers == null || headers.isEmpty()) { |
| return ""; //$NON-NLS-1$ |
| } |
| |
| List<String> sortedHeaders = new ArrayList<>(); |
| sortedHeaders.addAll(headers.keySet()); |
| Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER); |
| |
| StringBuilder buffer = new StringBuilder(); |
| for (String key : sortedHeaders) { |
| buffer.append( |
| key.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ") + ":" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| + headers.get(key).replaceAll("\\s+", " ")); //$NON-NLS-1$//$NON-NLS-2$ |
| buffer.append("\n"); //$NON-NLS-1$ |
| } |
| |
| return buffer.toString(); |
| } |
| |
| private static String dateStamp(Date now) { |
| // TODO(ms) cache and reuse DateFormat instances |
| SimpleDateFormat dateStampFormat = new SimpleDateFormat( |
| DATE_STRING_FORMAT); |
| dateStampFormat.setTimeZone(new SimpleTimeZone(0, UTC)); |
| String dateStamp = dateStampFormat.format(now); |
| return dateStamp; |
| } |
| |
| private static String dateTimeStampISO8601(Date now) { |
| // TODO(ms) cache and reuse DateFormat instances |
| SimpleDateFormat dateTimeFormat = new SimpleDateFormat( |
| ISO8601_BASIC_FORMAT); |
| dateTimeFormat.setTimeZone(new SimpleTimeZone(0, UTC)); |
| String dateTimeStamp = dateTimeFormat.format(now); |
| return dateTimeStamp; |
| } |
| |
| private static String scope(String region, String dateStamp) { |
| String scope = String.format("%s/%s/%s/%s", dateStamp, region, S3, //$NON-NLS-1$ |
| TERMINATOR); |
| return scope; |
| } |
| |
| private static String canonicalizeQueryString( |
| Map<String, String> parameters) { |
| if (parameters == null || parameters.isEmpty()) { |
| return ""; //$NON-NLS-1$ |
| } |
| |
| SortedMap<String, String> sorted = new TreeMap<>(); |
| |
| Iterator<Map.Entry<String, String>> pairs = parameters.entrySet() |
| .iterator(); |
| while (pairs.hasNext()) { |
| Map.Entry<String, String> pair = pairs.next(); |
| String key = pair.getKey(); |
| String value = pair.getValue(); |
| sorted.put(urlEncode(key, false), urlEncode(value, false)); |
| } |
| |
| StringBuilder builder = new StringBuilder(); |
| pairs = sorted.entrySet().iterator(); |
| while (pairs.hasNext()) { |
| Map.Entry<String, String> pair = pairs.next(); |
| builder.append(pair.getKey()); |
| builder.append("="); //$NON-NLS-1$ |
| builder.append(pair.getValue()); |
| if (pairs.hasNext()) { |
| builder.append("&"); //$NON-NLS-1$ |
| } |
| } |
| |
| return builder.toString(); |
| } |
| |
| private static String canonicalRequest(URL endpoint, String httpMethod, |
| String queryParameters, String canonicalizedHeaderNames, |
| String canonicalizedHeaders, String bodyHash) { |
| return String.format("%s\n%s\n%s\n%s\n%s\n%s", //$NON-NLS-1$ |
| httpMethod, canonicalizeResourcePath(endpoint), |
| queryParameters, canonicalizedHeaders, canonicalizedHeaderNames, |
| bodyHash); |
| } |
| |
| private static String canonicalizeResourcePath(URL endpoint) { |
| if (endpoint == null) { |
| return "/"; //$NON-NLS-1$ |
| } |
| String path = endpoint.getPath(); |
| if (path == null || path.isEmpty()) { |
| return "/"; //$NON-NLS-1$ |
| } |
| |
| String encodedPath = urlEncode(path, true); |
| if (encodedPath.startsWith("/")) { //$NON-NLS-1$ |
| return encodedPath; |
| } else { |
| return "/" + encodedPath; //$NON-NLS-1$ |
| } |
| } |
| |
| private static byte[] hash(String s) { |
| MessageDigest md = Constants.newMessageDigest(); |
| md.update(s.getBytes(StandardCharsets.UTF_8)); |
| return md.digest(); |
| } |
| |
| private static byte[] sign(String stringData, byte[] key) { |
| try { |
| byte[] data = stringData.getBytes("UTF-8"); //$NON-NLS-1$ |
| Mac mac = Mac.getInstance(HMACSHA256); |
| mac.init(new SecretKeySpec(key, HMACSHA256)); |
| return mac.doFinal(data); |
| } catch (Exception e) { |
| throw new RuntimeException(MessageFormat.format( |
| LfsServerText.get().failedToCalcSignature, e.getMessage()), |
| e); |
| } |
| } |
| |
| private static String stringToSign(String scheme, String algorithm, |
| String dateTime, String scope, String canonicalRequest) { |
| return String.format("%s-%s\n%s\n%s\n%s", //$NON-NLS-1$ |
| scheme, algorithm, dateTime, scope, |
| toHex(hash(canonicalRequest))); |
| } |
| |
| private static String toHex(byte[] bytes) { |
| StringBuilder builder = new StringBuilder(2 * bytes.length); |
| for (byte b : bytes) { |
| builder.append(HEX.charAt((b & 0xF0) >> 4)); |
| builder.append(HEX.charAt(b & 0xF)); |
| } |
| return builder.toString(); |
| } |
| |
| private static String urlEncode(String url, boolean keepPathSlash) { |
| String encoded; |
| try { |
| encoded = URLEncoder.encode(url, StandardCharsets.UTF_8.name()); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException(LfsServerText.get().unsupportedUtf8, e); |
| } |
| if (keepPathSlash) { |
| encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| return encoded; |
| } |
| |
| private static byte[] createSignature(S3Config bucketConfig, |
| String dateTimeStamp, String dateStamp, |
| String scope, String canonicalRequest) { |
| String stringToSign = stringToSign(SCHEME, ALGORITHM, dateTimeStamp, |
| scope, canonicalRequest); |
| |
| byte[] signature = (SCHEME + bucketConfig.getSecretKey()).getBytes(); |
| signature = sign(dateStamp, signature); |
| signature = sign(bucketConfig.getRegion(), signature); |
| signature = sign(S3, signature); |
| signature = sign(TERMINATOR, signature); |
| signature = sign(stringToSign, signature); |
| return signature; |
| } |
| } |