AmazonS3: Add support for AWS API signature version 4

Updating the AmazonS3 class to support AWS Signature version 4 because
version 2 is no longer supported in all AWS regions. The version can be
selected with the new 'aws.api.signature.version' property (defaults to
2 for backwards compatibility). When set to '4', the user must also
specify the AWS region via the 'region' property. The 'region' property
must match the region that the 'domain' property resolves to.

Bug: 579907
Change-Id: If289dbc6d0f57323cfeaac2624c4eb5028f78d13
This commit is contained in:
eric.steele 2022-05-31 18:03:17 -07:00 committed by Matthias Sohn
parent 5efd32e91d
commit e9a5430c25
6 changed files with 439 additions and 14 deletions

View File

@ -40,6 +40,15 @@
# * https://docs.aws.amazon.com/AmazonS3/latest/dev/manage-lifecycle-using-console.html
#
# AWS API signature version (defaults to 2)
# aws.api.signature.version=4
# AWS S3 Region Domain (defaults to s3.amazonaws.com)
# domain: s3-us-east-2.amazonaws.com
# AWS S3 Region (required if aws.api.signature.version=4, must match domain)
# region: us-east-2
# Test bucket name
test.bucket=jgit.eclipse.org

View File

@ -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
@ -361,6 +363,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
@ -457,6 +460,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}
@ -738,6 +742,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

View File

@ -238,12 +238,14 @@ public static JGitText get() {
/***/ 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;
@ -389,6 +391,7 @@ public static JGitText get() {
/***/ public String inTheFuture;
/***/ public String invalidAdvertisementOf;
/***/ public String invalidAncestryLength;
/***/ public String invalidAwsApiSignatureVersion;
/***/ public String invalidBooleanValue;
/***/ public String invalidChannel;
/***/ public String invalidCommitParentNumber;
@ -485,6 +488,7 @@ public static JGitText get() {
/***/ public String mismatchOffset;
/***/ public String mismatchCRC;
/***/ public String missingAccesskey;
/***/ public String missingAwsRegion;
/***/ public String missingConfigurationForKey;
/***/ public String missingCookieFile;
/***/ public String missingCRC;
@ -766,6 +770,7 @@ public static JGitText get() {
/***/ public String unableToSignCommitNoSecretKey;
/***/ public String unauthorized;
/***/ public String unencodeableFile;
/***/ public String unexpectedAwsApiSignatureVersion;
/***/ public String unexpectedCompareResult;
/***/ public String unexpectedEndOfConfigFile;
/***/ public String unexpectedEndOfInput;

View File

@ -31,6 +31,7 @@
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -46,7 +47,6 @@
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.time.Instant;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@ -85,6 +85,12 @@
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$
@ -134,11 +140,17 @@ private static MessageDigest newMD5() {
}
}
/** 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;
@ -158,8 +170,12 @@ private static MessageDigest newMD5() {
/** 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$
@ -167,6 +183,7 @@ interface Keys {
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$
}
@ -179,6 +196,12 @@ interface Keys {
* 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: &lt;YourAWSAccessKey&gt;
* secretkey: &lt;YourAWSSecretKey&gt;
@ -191,6 +214,9 @@ interface Keys {
* # 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
*
@ -203,16 +229,34 @@ interface Keys {
* 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$
@ -257,7 +301,7 @@ public URLConnection get(String bucket, String key)
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);
@ -338,7 +382,7 @@ public void delete(String bucket, String key)
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;
@ -384,13 +428,16 @@ public void put(String bucket, String key, byte[] data)
}
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()) {
@ -465,6 +512,9 @@ void putImpl(final String bucket, final String key,
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$
@ -472,7 +522,7 @@ void putImpl(final String bucket, final String key,
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()) {
@ -544,8 +594,13 @@ HttpURLConnection open(final String method, final String bucket,
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;
@ -572,7 +627,18 @@ HttpURLConnection open(final String method, final String bucket,
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()) {
@ -609,7 +675,7 @@ void authorize(HttpURLConnection c) throws IOException {
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()));
@ -673,7 +739,7 @@ void list() throws IOException {
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;

View File

@ -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);
}
}
}

View File

@ -185,6 +185,33 @@ public static void encode(StringBuilder urlstr, String key) {
}
}
/**
* 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>