Skip to content

Instantly share code, notes, and snippets.

@vincentjames501
Created February 15, 2021 06:11
Show Gist options
  • Save vincentjames501/15dfb0a18b61fa0ab0984c5d98f29cd5 to your computer and use it in GitHub Desktop.
Save vincentjames501/15dfb0a18b61fa0ab0984c5d98f29cd5 to your computer and use it in GitHub Desktop.
A lightweight Java method to generate presigned S3 GET URLs fast and with no dependencies
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
public class FastS3Signer {
private static final String HMAC_SHA_256_ALGORITHM = "HmacSHA256";
private static final DateFormat AMZ_DATE_FORMATTER = makeUTC(new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"));
private static final DateFormat DATESTAMP_FORMATTER = makeUTC(new SimpleDateFormat("yyyyMMdd"));
/**
* Given a DateFormat object, make use UTC TimeZone.
*/
private static DateFormat makeUTC(DateFormat dateFormat) {
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat;
}
/**
* Performs Hmac Sha256 on the input data. See:
* https://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-java
*/
private static byte[] HmacSHA256(final String data, final byte[] key) throws Exception {
Mac mac = Mac.getInstance(HMAC_SHA_256_ALGORITHM);
mac.init(new SecretKeySpec(key, HMAC_SHA_256_ALGORITHM));
return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
}
/**
* Key derivation functions. See:
* https://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-java
*/
private static byte[] getSignatureKey(final String key, final String dateStamp, final String regionName, final String serviceName) throws Exception {
byte[] kSecret = ("AWS4" + key).getBytes(StandardCharsets.UTF_8);
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
return HmacSHA256("aws4_request", kService);
}
/**
* Helper function to sha256 the input data
*/
private static byte[] sha256(final String originalString) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(originalString.getBytes(StandardCharsets.UTF_8));
}
/**
* Helper function to convert bytes to a hex string
*/
private static String bytesToHex(final byte[] hash) {
StringBuilder hexString = new StringBuilder(2 * hash.length);
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
public static String signRequest(
final String accessKey,
final String secretKey,
final String httpMethod,
final String path,
final String host,
final String region,
final String awsService,
final String endpoint,
final Date timestamp,
final int expiration
) throws Exception {
// Create a date for headers and the credential string
String amzDate = AMZ_DATE_FORMATTER.format(timestamp);
String datestamp = DATESTAMP_FORMATTER.format(timestamp);
// ************* TASK 1: CREATE A CANONICAL REQUEST *************
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
// Step 1: Define the verb (GET, POST, etc.) -- supplied in method above.
// Step 2: Create canonical URI--the part of the URI from domain to query
// string (use '/' if no path) -- supplied in path above
// Step 3: Create the canonical headers and signed headers. Header names
// must be trimmed and lowercase, and sorted in code point order from
// low to high. Note trailing \n in canonicalHeaders.
// signedHeaders is the list of headers that are being included
// as part of the signing process. For requests that use query strings,
// only "host" is included in the signed headers.
String canonicalHeaders = "host:" + host + "\n";
String signedHeaders = "host";
// Match the algorithm to the hashing algorithm you use, either SHA-1 or
// SHA-256 (recommended)
String algorithm = "AWS4-HMAC-SHA256";
String credentialScope = datestamp + "/" + region + "/" + awsService + "/" + "aws4_request";
// Step 4: Create the canonical query string. In this example, request
// parameters are in the query string. Query string values must
// be URL-encoded (space=%20). The parameters must be sorted by name.
String canonicalQueryString = "X-Amz-Algorithm=" + algorithm
+ "&X-Amz-Credential=" + URLEncoder.encode(accessKey + "/" + credentialScope, StandardCharsets.UTF_8)
+ "&X-Amz-Date=" + amzDate
+ "&X-Amz-Expires=" + expiration
+ "&X-Amz-SignedHeaders=" + signedHeaders;
// Step 5: Create payload hash. For GET requests, the payload is an
// empty string (""). Where the content hash is unknown, use "UNSIGNED-PAYLOAD".
String payloadHash = "UNSIGNED-PAYLOAD";
// Step 6: Combine elements to create canonical request
String canonicalRequest = httpMethod + "\n" + path + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + payloadHash;
// ************* TASK 2: CREATE THE STRING TO SIGN*************
String stringToSign = algorithm + "\n" + amzDate + "\n" + credentialScope + "\n" + bytesToHex(sha256(canonicalRequest));
// ************* TASK 3: CALCULATE THE SIGNATURE *************
// Create the signing key
byte[] signingKey = getSignatureKey(secretKey, datestamp, region, awsService);
// Sign the stringToSign using the signing_key
String signature = bytesToHex(HmacSHA256(stringToSign, signingKey));
// ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
// The auth information can be either in a query string
// value or in a header named Authorization. This code shows how to put
// everything into a query string.
return endpoint + path + "?" + canonicalQueryString + "&X-Amz-Signature=" + signature;
}
public static String signS3GETRequest(
final String accessKey,
final String secretKey,
final String region,
final String bucket,
final String key,
final Date timestamp,
final int expiration
) throws Exception {
return signRequest(
accessKey,
secretKey,
"GET",
"/" + key,
bucket + ".s3.amazonaws.com",
region,
"s3",
"https://" + bucket + ".s3.amazonaws.com",
timestamp,
expiration
);
}
public static void main(String[] args) throws Exception {
final String ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE";
final String SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
System.out.println(signS3GETRequest(ACCESS_KEY, SECRET_KEY, "us-east-1", "examplebucket", "test.txt", new Date(1369353600000L), 86400));
// Warm up
for (int i = 0; i < 100000; i++) {
signS3GETRequest(
ACCESS_KEY,
SECRET_KEY,
"us-east-1",
"examplebucket",
"test" + i + ".txt",
new Date(1369353600000L),
86400
);
}
Date start = new Date();
for (int i = 0; i < 100000; i++) {
signS3GETRequest(
ACCESS_KEY,
SECRET_KEY,
"us-east-1",
"examplebucket",
"test" + i + ".txt",
new Date(1369353600000L),
86400
);
}
System.out.println("Done: " + (new Date().getTime() - start.getTime()));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment