diff options
author | Matthias Sohn <matthias.sohn@sap.com> | 2022-06-15 17:39:52 +0200 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2022-06-15 17:39:52 +0200 |
commit | 58f5302e1d8c1c7a4111c141bea0533c41129a74 (patch) | |
tree | 80b4c1cb1b30f9c3124c6e73b644c13d74cb8fec /org.eclipse.jgit | |
parent | a9f535e104e66180942a4cfdcb6c8e686b8cb951 (diff) | |
parent | d0bc2b544a71e4150bfb31d0f0c9348ac076499b (diff) | |
download | jgit-58f5302e1d8c1c7a4111c141bea0533c41129a74.tar.gz jgit-58f5302e1d8c1c7a4111c141bea0533c41129a74.zip |
Merge branch 'stable-6.1' into stable-6.2
* stable-6.1:
Prepare 5.13.2-SNAPSHOT builds
JGit v5.13.1.202206130422-r
AmazonS3: Add support for AWS API signature version 4
Change-Id: Id4965aacd4e2ea1e8575a2c1bd4845729db6049a
Diffstat (limited to 'org.eclipse.jgit')
5 files changed, 429 insertions, 13 deletions
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index f0bb6c6c99..66adad5151 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -209,12 +209,14 @@ couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This R couldNotGetRepoStatistics=Could not get repository statistics couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format. couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format. +couldNotHashByteArrayWithSha256=Could not hash byte array with SHA-256 algorithm. couldNotLockHEAD=Could not lock HEAD couldNotPersistCookies=Could not persist received cookies in file ''{0}'' couldNotReadCookieFile=Could not read cookie file ''{0}'' couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0} couldNotRewindToUpstreamCommit=Could not rewind to upstream commit +couldNotSignStringWithKey=Could not sign string with key. couldNotURLEncodeToUTF8=Could not URL encode to UTF-8 countingObjects=Counting objects corruptPack=Pack file {0} is corrupt, removing it from pack list @@ -366,6 +368,7 @@ interruptedWriting=Interrupted writing {0} inTheFuture=in the future invalidAdvertisementOf=invalid advertisement of {0} invalidAncestryLength=Invalid ancestry length +invalidAwsApiSignatureVersion=Invalid aws.api.signature.version: {0} invalidBooleanValue=Invalid boolean value: {0}.{1}={2} invalidChannel=Invalid channel {0} invalidCommitParentNumber=Invalid commit parent number @@ -467,6 +470,7 @@ minutesAgo={0} minutes ago mismatchOffset=mismatch offset for object {0} mismatchCRC=mismatch CRC for object {0} missingAccesskey=Missing accesskey. +missingAwsRegion=Missing region (e.g. us-west-2). missingConfigurationForKey=No value for key {0} found in configuration missingCookieFile=Configured http.cookieFile ''{0}'' is missing missingCRC=missing CRC for object {0} @@ -753,6 +757,7 @@ unableToWrite=Unable to write {0} unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available. unauthorized=Unauthorized unencodeableFile=Unencodable file: {0} +unexpectedAwsApiSignatureVersion=Unexpected AWS API Signature Version: {0} unexpectedCompareResult=Unexpected metadata comparison result: {0} unexpectedEndOfConfigFile=Unexpected end of config file unexpectedEndOfInput=Unexpected end of input diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 17e359de49..efdb8e42e3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -238,12 +238,14 @@ public class JGitText extends TranslationBundle { /***/ public String couldNotFindSixTabsInLine; /***/ public String couldNotGetAdvertisedRef; /***/ public String couldNotGetRepoStatistics; + /***/ public String couldNotHashByteArrayWithSha256; /***/ public String couldNotLockHEAD; /***/ public String couldNotPersistCookies; /***/ public String couldNotReadCookieFile; /***/ public String couldNotReadIndexInOneGo; /***/ public String couldNotReadObjectWhileParsingCommit; /***/ public String couldNotRewindToUpstreamCommit; + /***/ public String couldNotSignStringWithKey; /***/ public String couldNotURLEncodeToUTF8; /***/ public String countingObjects; /***/ public String createBranchFailedUnknownReason; @@ -394,6 +396,7 @@ public class JGitText extends TranslationBundle { /***/ public String inTheFuture; /***/ public String invalidAdvertisementOf; /***/ public String invalidAncestryLength; + /***/ public String invalidAwsApiSignatureVersion; /***/ public String invalidBooleanValue; /***/ public String invalidChannel; /***/ public String invalidCommitParentNumber; @@ -495,6 +498,7 @@ public class JGitText extends TranslationBundle { /***/ public String mismatchOffset; /***/ public String mismatchCRC; /***/ public String missingAccesskey; + /***/ public String missingAwsRegion; /***/ public String missingConfigurationForKey; /***/ public String missingCookieFile; /***/ public String missingCRC; @@ -781,6 +785,7 @@ public class JGitText extends TranslationBundle { /***/ public String unableToSignCommitNoSecretKey; /***/ public String unauthorized; /***/ public String unencodeableFile; + /***/ public String unexpectedAwsApiSignatureVersion; /***/ public String unexpectedCompareResult; /***/ public String unexpectedEndOfConfigFile; /***/ public String unexpectedEndOfInput; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java index d56b5b320d..81a70af2d2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java @@ -86,6 +86,12 @@ import org.xml.sax.helpers.DefaultHandler; public class AmazonS3 { private static final Set<String> SIGNED_HEADERS; + private static final String AWS_API_V2 = "2"; //$NON-NLS-1$ + + private static final String AWS_API_V4 = "4"; //$NON-NLS-1$ + + private static final String AWS_S3_SERVICE_NAME = "s3"; //$NON-NLS-1$ + private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$ private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$ @@ -135,11 +141,17 @@ public class AmazonS3 { } } + /** AWS API Signature Version. */ + private final String awsApiSignatureVersion; + /** AWSAccessKeyId, public string that identifies the user's account. */ private final String publicKey; /** Decoded form of the private AWSSecretAccessKey, to sign requests. */ - private final SecretKeySpec privateKey; + private final SecretKeySpec secretKeySpec; + + /** AWSSecretAccessKey, private string used to access a user's account. */ + private final char[] secretKey; // store as char[] for security /** Our HTTP proxy support, in case we are behind a firewall. */ private final ProxySelector proxySelector; @@ -159,8 +171,12 @@ public class AmazonS3 { /** S3 Bucket Domain. */ private final String domain; + /** S3 Region. */ + private final String region; + /** Property names used in amazon connection configuration file. */ interface Keys { + String AWS_API_SIGNATURE_VERSION = "aws.api.signature.version"; //$NON-NLS-1$ String ACCESS_KEY = "accesskey"; //$NON-NLS-1$ String SECRET_KEY = "secretkey"; //$NON-NLS-1$ String PASSWORD = "password"; //$NON-NLS-1$ @@ -168,6 +184,7 @@ public class AmazonS3 { String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$ String ACL = "acl"; //$NON-NLS-1$ String DOMAIN = "domain"; //$NON-NLS-1$ + String REGION = "region"; //$NON-NLS-1$ String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$ String TMP_DIR = "tmpdir"; //$NON-NLS-1$ } @@ -180,6 +197,12 @@ public class AmazonS3 { * For example: * * <pre> + * # AWS API signature version, must be one of: + * # 2 - deprecated (not supported in all AWS regions) + * # 4 - latest (supported in all AWS regions) + * # Defaults to 2. + * aws.api.signature.version: 4 + * * # AWS Access and Secret Keys (required) * accesskey: <YourAWSAccessKey> * secretkey: <YourAWSSecretKey> @@ -192,6 +215,9 @@ public class AmazonS3 { * # AWS S3 Region Domain (defaults to s3.amazonaws.com) * domain: s3.amazonaws.com * + * # AWS S3 Region (required if aws.api.signature.version = 4) + * region: us-west-2 + * * # Number of times to retry after internal error from S3. * httpclient.retry-max: 3 * @@ -204,16 +230,34 @@ public class AmazonS3 { * connection properties. */ public AmazonS3(final Properties props) { + awsApiSignatureVersion = props + .getProperty(Keys.AWS_API_SIGNATURE_VERSION, AWS_API_V2); + if (awsApiSignatureVersion.equals(AWS_API_V4)) { + region = props.getProperty(Keys.REGION); + if (region == null) { + throw new IllegalArgumentException( + JGitText.get().missingAwsRegion); + } + } else if (awsApiSignatureVersion.equals(AWS_API_V2)) { + region = null; + } else { + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().invalidAwsApiSignatureVersion, + awsApiSignatureVersion)); + } + domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$ publicKey = props.getProperty(Keys.ACCESS_KEY); if (publicKey == null) throw new IllegalArgumentException(JGitText.get().missingAccesskey); - final String secret = props.getProperty(Keys.SECRET_KEY); - if (secret == null) + final String secretKeyStr = props.getProperty(Keys.SECRET_KEY); + if (secretKeyStr == null) { throw new IllegalArgumentException(JGitText.get().missingSecretkey); - privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC); + } + secretKeySpec = new SecretKeySpec(Constants.encodeASCII(secretKeyStr), HMAC); + secretKey = secretKeyStr.toCharArray(); final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$ if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$ @@ -258,7 +302,7 @@ public class AmazonS3 { throws IOException { for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("GET", bucket, key); //$NON-NLS-1$ - authorize(c); + authorize(c, Collections.emptyMap(), 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_OK: encryption.validate(c, X_AMZ_META); @@ -339,7 +383,7 @@ public class AmazonS3 { throws IOException { for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("DELETE", bucket, key); //$NON-NLS-1$ - authorize(c); + authorize(c, Collections.emptyMap(), 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_NO_CONTENT: return; @@ -385,13 +429,16 @@ public class AmazonS3 { } final String md5str = Base64.encodeBytes(newMD5().digest(data)); + final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4) + ? AwsRequestSignerV4.calculateBodyHash(data) + : null; final String lenstr = String.valueOf(data.length); for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$ c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$ c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$ c.setRequestProperty(X_AMZ_ACL, acl); - authorize(c); + authorize(c, Collections.emptyMap(), data.length, bodyHash); c.setDoOutput(true); c.setFixedLengthStreamingMode(data.length); try (OutputStream os = c.getOutputStream()) { @@ -466,6 +513,9 @@ public class AmazonS3 { monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key); final String md5str = Base64.encodeBytes(csum); + final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4) + ? AwsRequestSignerV4.calculateBodyHash(buf.toByteArray()) + : null; final long len = buf.length(); for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$ @@ -473,7 +523,7 @@ public class AmazonS3 { c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$ c.setRequestProperty(X_AMZ_ACL, acl); encryption.request(c, X_AMZ_META); - authorize(c); + authorize(c, Collections.emptyMap(), len, bodyHash); c.setDoOutput(true); monitor.beginTask(monitorTask, (int) (len / 1024)); try (OutputStream os = c.getOutputStream()) { @@ -545,8 +595,13 @@ public class AmazonS3 { urlstr.append('.'); urlstr.append(domain); urlstr.append('/'); - if (key.length() > 0) - HttpSupport.encode(urlstr, key); + if (key.length() > 0) { + if (awsApiSignatureVersion.equals(AWS_API_V2)) { + HttpSupport.encode(urlstr, key); + } else if (awsApiSignatureVersion.equals(AWS_API_V4)) { + urlstr.append(key); + } + } if (!args.isEmpty()) { final Iterator<Map.Entry<String, String>> i; @@ -573,7 +628,18 @@ public class AmazonS3 { return c; } - void authorize(HttpURLConnection c) throws IOException { + void authorize(HttpURLConnection httpURLConnection, + Map<String, String> queryParams, long contentLength, + final String bodyHash) throws IOException { + if (awsApiSignatureVersion.equals(AWS_API_V2)) { + authorizeV2(httpURLConnection); + } else if (awsApiSignatureVersion.equals(AWS_API_V4)) { + AwsRequestSignerV4.sign(httpURLConnection, queryParams, contentLength, bodyHash, AWS_S3_SERVICE_NAME, + region, publicKey, secretKey); + } + } + + void authorizeV2(HttpURLConnection c) throws IOException { final Map<String, List<String>> reqHdr = c.getRequestProperties(); final SortedMap<String, String> sigHdr = new TreeMap<>(); for (Map.Entry<String, List<String>> entry : reqHdr.entrySet()) { @@ -610,7 +676,7 @@ public class AmazonS3 { final String sec; try { final Mac m = Mac.getInstance(HMAC); - m.init(privateKey); + m.init(secretKeySpec); sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes(UTF_8))); } catch (NoSuchAlgorithmException e) { throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage())); @@ -674,7 +740,7 @@ public class AmazonS3 { for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$ - authorize(c); + authorize(c, args, 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_OK: truncated = false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java new file mode 100644 index 0000000000..6b3d39721a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2022, Workday Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.transport; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.util.Hex; +import org.eclipse.jgit.util.HttpSupport; + +/** + * Utility class for signing requests to AWS service endpoints using the V4 + * signing protocol. + * + * Reference implementation: <a href= + * "https://docs.aws.amazon.com/AmazonS3/latest/API/samples/AWSS3SigV4JavaSamples.zip">AWSS3SigV4JavaSamples.zip</a> + * + * @see <a href= + * "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html">AWS + * Signature Version 4</a> + * + * @since 5.13 + */ +public final class AwsRequestSignerV4 { + + /** AWS version 4 signing algorithm (for authorization header). **/ + private static final String ALGORITHM = "HMAC-SHA256"; //$NON-NLS-1$ + + /** Java Message Authentication Code (MAC) algorithm name. **/ + private static final String MAC_ALGORITHM = "HmacSHA256"; //$NON-NLS-1$ + + /** AWS version 4 signing scheme. **/ + private static final String SCHEME = "AWS4"; //$NON-NLS-1$ + + /** AWS version 4 terminator string. **/ + private static final String TERMINATOR = "aws4_request"; //$NON-NLS-1$ + + /** SHA-256 hash of an empty request body. **/ + private static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; //$NON-NLS-1$ + + /** Date format for the 'x-amz-date' header. **/ + private static final DateTimeFormatter AMZ_DATE_FORMAT = DateTimeFormatter + .ofPattern("yyyyMMdd'T'HHmmss'Z'"); //$NON-NLS-1$ + + /** Date format for the string-to-sign's scope. **/ + private static final DateTimeFormatter SCOPE_DATE_FORMAT = DateTimeFormatter + .ofPattern("yyyyMMdd"); //$NON-NLS-1$ + + private AwsRequestSignerV4() { + // Don't instantiate utility class + } + + /** + * Sign the provided request with an AWS4 signature as the 'Authorization' + * header. + * + * @param httpURLConnection + * The request to sign. + * @param queryParameters + * The query parameters being sent in the request. + * @param contentLength + * The content length of the data being sent in the request + * @param bodyHash + * Hex-encoded SHA-256 hash of the data being sent in the request + * @param serviceName + * The signing name of the AWS service (e.g. "s3"). + * @param regionName + * The name of the AWS region that will handle the request (e.g. + * "us-east-1"). + * @param awsAccessKey + * The user's AWS Access Key. + * @param awsSecretKey + * The user's AWS Secret Key. + */ + public static void sign(HttpURLConnection httpURLConnection, + Map<String, String> queryParameters, long contentLength, + String bodyHash, String serviceName, String regionName, + String awsAccessKey, char[] awsSecretKey) { + // get request headers + Map<String, String> headers = new HashMap<>(); + httpURLConnection.getRequestProperties() + .forEach((headerName, headerValues) -> headers.put(headerName, + String.join(",", headerValues))); //$NON-NLS-1$ + + // add required content headers + if (contentLength > 0) { + headers.put(HttpSupport.HDR_CONTENT_LENGTH, + String.valueOf(contentLength)); + } else { + bodyHash = EMPTY_BODY_SHA256; + } + headers.put("x-amz-content-sha256", bodyHash); //$NON-NLS-1$ + + // add the 'x-amz-date' header + OffsetDateTime now = Instant.now().atOffset(ZoneOffset.UTC); + String amzDate = now.format(AMZ_DATE_FORMAT); + headers.put("x-amz-date", amzDate); //$NON-NLS-1$ + + // add the 'host' header + URL endpointUrl = httpURLConnection.getURL(); + int port = endpointUrl.getPort(); + String hostHeader = (port > -1) + ? endpointUrl.getHost().concat(":" + port) //$NON-NLS-1$ + : endpointUrl.getHost(); + headers.put("Host", hostHeader); //$NON-NLS-1$ + + // construct the canonicalized request + String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers); + String canonicalizedHeaders = getCanonicalizedHeaderString(headers); + String canonicalizedQueryParameters = getCanonicalizedQueryString( + queryParameters); + String httpMethod = httpURLConnection.getRequestMethod(); + String canonicalRequest = httpMethod + '\n' + + getCanonicalizedResourcePath(endpointUrl) + '\n' + + canonicalizedQueryParameters + '\n' + canonicalizedHeaders + + '\n' + canonicalizedHeaderNames + '\n' + bodyHash; + + // construct the string-to-sign + String scopeDate = now.format(SCOPE_DATE_FORMAT); + String scope = scopeDate + '/' + regionName + '/' + serviceName + '/' + + TERMINATOR; + String stringToSign = SCHEME + '-' + ALGORITHM + '\n' + amzDate + '\n' + + scope + '\n' + Hex.toHexString(hash( + canonicalRequest.getBytes(StandardCharsets.UTF_8))); + + // compute the signing key + byte[] secretKey = (SCHEME + new String(awsSecretKey)).getBytes(); + byte[] dateKey = signStringWithKey(scopeDate, secretKey); + byte[] regionKey = signStringWithKey(regionName, dateKey); + byte[] serviceKey = signStringWithKey(serviceName, regionKey); + byte[] signingKey = signStringWithKey(TERMINATOR, serviceKey); + byte[] signature = signStringWithKey(stringToSign, signingKey); + + // construct the authorization header + String credentialsAuthorizationHeader = "Credential=" + awsAccessKey //$NON-NLS-1$ + + '/' + scope; + String signedHeadersAuthorizationHeader = "SignedHeaders=" //$NON-NLS-1$ + + canonicalizedHeaderNames; + String signatureAuthorizationHeader = "Signature=" //$NON-NLS-1$ + + Hex.toHexString(signature); + String authorizationHeader = SCHEME + '-' + ALGORITHM + ' ' + + credentialsAuthorizationHeader + ", " //$NON-NLS-1$ + + signedHeadersAuthorizationHeader + ", " //$NON-NLS-1$ + + signatureAuthorizationHeader; + + // Copy back the updated request headers + headers.forEach(httpURLConnection::setRequestProperty); + + // Add the 'authorization' header + httpURLConnection.setRequestProperty(HttpSupport.HDR_AUTHORIZATION, + authorizationHeader); + } + + /** + * Calculates the hex-encoded SHA-256 hash of the provided byte array. + * + * @param data + * Byte array to hash + * + * @return Hex-encoded SHA-256 hash of the provided byte array. + */ + public static String calculateBodyHash(final byte[] data) { + return (data == null || data.length < 1) ? EMPTY_BODY_SHA256 + : Hex.toHexString(hash(data)); + } + + /** + * Construct a string listing all request headers in sorted case-insensitive + * order, separated by a ';'. + * + * @param headers + * Map containing all request headers. + * + * @return String that lists all request headers in sorted case-insensitive + * order, separated by a ';'. + */ + private static String getCanonicalizeHeaderNames( + Map<String, String> headers) { + return headers.keySet().stream().map(String::toLowerCase).sorted() + .collect(Collectors.joining(";")); //$NON-NLS-1$ + } + + /** + * Constructs the canonical header string for a request. + * + * @param headers + * Map containing all request headers. + * + * @return The canonical headers with values for the request. + */ + private static String getCanonicalizedHeaderString( + Map<String, String> headers) { + if (headers == null || headers.isEmpty()) { + return ""; //$NON-NLS-1$ + } + StringBuilder sb = new StringBuilder(); + headers.keySet().stream().sorted(String.CASE_INSENSITIVE_ORDER) + .forEach(key -> { + String header = key.toLowerCase().replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + String value = headers.get(key).replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + sb.append(header).append(':').append(value).append('\n'); + }); + return sb.toString(); + } + + /** + * Constructs the canonicalized resource path for an AWS service endpoint. + * + * @param url + * The AWS service endpoint URL, including the path to any + * resource. + * + * @return The canonicalized resource path for the AWS service endpoint. + */ + private static String getCanonicalizedResourcePath(URL url) { + if (url == null) { + return "/"; //$NON-NLS-1$ + } + String path = url.getPath(); + if (path == null || path.isEmpty()) { + return "/"; //$NON-NLS-1$ + } + String encodedPath = HttpSupport.urlEncode(path, true); + if (encodedPath.startsWith("/")) { //$NON-NLS-1$ + return encodedPath; + } + return "/".concat(encodedPath); //$NON-NLS-1$ + } + + /** + * Constructs the canonicalized query string for a request. + * + * @param queryParameters + * The query parameters in the request. + * + * @return The canonicalized query string for the request. + */ + public static String getCanonicalizedQueryString( + Map<String, String> queryParameters) { + if (queryParameters == null || queryParameters.isEmpty()) { + return ""; //$NON-NLS-1$ + } + return queryParameters + .keySet().stream().sorted().map( + key -> HttpSupport.urlEncode(key, false) + '=' + + HttpSupport.urlEncode( + queryParameters.get(key), false)) + .collect(Collectors.joining("&")); //$NON-NLS-1$ + } + + /** + * Hashes the provided byte array using the SHA-256 algorithm. + * + * @param data + * The byte array to hash. + * + * @return Hashed string contents of the provided byte array. + */ + public static byte[] hash(byte[] data) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$ + md.update(data); + return md.digest(); + } catch (Exception e) { + throw new RuntimeException( + JGitText.get().couldNotHashByteArrayWithSha256, e); + } + } + + /** + * Signs the provided string data using the specified key. + * + * @param stringToSign + * The string data to sign. + * @param key + * The key material of the secret key. + * + * @return Signed string data. + */ + private static byte[] signStringWithKey(String stringToSign, byte[] key) { + try { + byte[] data = stringToSign.getBytes(StandardCharsets.UTF_8); + Mac mac = Mac.getInstance(MAC_ALGORITHM); + mac.init(new SecretKeySpec(key, MAC_ALGORITHM)); + return mac.doFinal(data); + } catch (Exception e) { + throw new RuntimeException(JGitText.get().couldNotSignStringWithKey, + e); + } + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java index 23a73faf8c..663a3449e1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -186,6 +186,33 @@ public class HttpSupport { } /** + * Translates the provided URL into application/x-www-form-urlencoded + * format. + * + * @param url + * The URL to translate. + * @param keepPathSlash + * Whether or not to keep "/" in the URL (i.e. don't translate + * them to "%2F"). + * + * @return The translated URL. + * @since 5.13 + */ + public static String urlEncode(String url, boolean keepPathSlash) { + String encoded; + try { + encoded = URLEncoder.encode(url, UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8, + e); + } + if (keepPathSlash) { + encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + } + return encoded; + } + + /** * Get the HTTP response code from the request. * <p> * Roughly the same as <code>c.getResponseCode()</code> but the |