Skip to content

Instantly share code, notes, and snippets.

@Eng-Fouad
Created October 14, 2024 01:34
Show Gist options
  • Save Eng-Fouad/e35959fbdf984df7a5d1fd3ec880fef1 to your computer and use it in GitHub Desktop.
Save Eng-Fouad/e35959fbdf984df7a5d1fd3ec880fef1 to your computer and use it in GitHub Desktop.
Sign Minio/AWS S3 url manually in Java without dependencies
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class UrlSigner {
private static final ZoneId UTC = ZoneId.of("Z");
private static final DateTimeFormatter SIGNER_DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMdd", Locale.US).withZone(UTC);
public static final DateTimeFormatter AMZ_DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US).withZone(UTC);
private final String minioEndpoint; // e.g. https://example.com:12345
private final String minioBucket; // e.g. files
private final String minioRegion; // e.g. sa
private final String minioAccessKey;
private final String minioSecretKey;
public UrlSigner(String minioEndpoint, String minioBucket, String minioRegion, String minioAccessKey, String minioSecretKey) {
this.minioEndpoint = minioEndpoint;
this.minioBucket = minioBucket;
this.minioRegion = minioRegion;
this.minioAccessKey = minioAccessKey;
this.minioSecretKey = minioSecretKey;
}
public String getPreSignedUrl(String httpMethod, String objectId) {
ZonedDateTime now = ZonedDateTime.now();
return getPreSignedUrl(httpMethod, objectId, now);
}
public String getPreSignedUrl(String httpMethod, String objectId, ZonedDateTime now) {
try {
String endpoint = minioEndpoint;
if (minioEndpoint.startsWith("https://") && minioEndpoint.endsWith(":443")) {
endpoint = minioEndpoint.substring(0, endpoint.lastIndexOf(":443"));
} else if (minioEndpoint.startsWith("http://") && minioEndpoint.endsWith(":80")) {
endpoint = minioEndpoint.substring(0, endpoint.lastIndexOf(":80"));
}
long expireInSeconds = TimeUnit.HOURS.toSeconds(1);
String algorithm = "AWS4-HMAC-SHA256";
String signedHeaders = "host";
String serviceName = "s3";
String scope = "%s/%s/%s/aws4_request".formatted(now.format(SIGNER_DATE_FORMAT), minioRegion, serviceName);
String objectName = Arrays.stream(objectId.split("/")).map(UrlSigner::encodeS3Url).collect(Collectors.joining("/"));
String query = "%s=%s&%s=%s&%s=%s&%s=%s&%s=%s".formatted(
encodeS3Url("X-Amz-Algorithm"), encodeS3Url(algorithm),
encodeS3Url("X-Amz-Credential"), encodeS3Url(minioAccessKey + "/" + scope),
encodeS3Url("X-Amz-Date"), encodeS3Url(now.format(AMZ_DATE_FORMAT)),
encodeS3Url("X-Amz-Expires"), encodeS3Url(Long.toString(expireInSeconds)),
encodeS3Url("X-Amz-SignedHeaders"), encodeS3Url(signedHeaders));
String path = "/%s/%s".formatted(minioBucket, objectName);
Map<String, String> canonicalHeaders = Map.of("host", endpoint.substring(endpoint.indexOf("://") + 3));
String contentSha256 = "UNSIGNED-PAYLOAD";
String canonicalRequest = "%s\n%s\n%s\n%s\n\n%s\n%s".formatted(httpMethod, path, query,
canonicalHeaders.keySet().stream().map(key -> key + ":" + canonicalHeaders.get(key)).collect(Collectors.joining("\n")),
String.join(";", canonicalHeaders.keySet()), contentSha256);
byte[] canonicalRequestBytes = canonicalRequest.getBytes(StandardCharsets.UTF_8);
MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256");
sha256Digest.update(canonicalRequestBytes, 0, canonicalRequestBytes.length);
String canonicalRequestHash = HexFormat.of().formatHex(sha256Digest.digest());
String stringToSign = "%s\n%s\n%s\n%s".formatted(algorithm, now.format(AMZ_DATE_FORMAT), scope, canonicalRequestHash);
String aws4SecretKey = "AWS4" + minioSecretKey;
byte[] dateKey = sumHmac(aws4SecretKey.getBytes(StandardCharsets.UTF_8), now.format(SIGNER_DATE_FORMAT).getBytes(StandardCharsets.UTF_8));
byte[] dateRegionKey = sumHmac(dateKey, minioRegion.getBytes(StandardCharsets.UTF_8));
byte[] dateRegionServiceKey = sumHmac(dateRegionKey, serviceName.getBytes(StandardCharsets.UTF_8));
byte[] signingKey = sumHmac(dateRegionServiceKey, "aws4_request".getBytes(StandardCharsets.UTF_8));
byte[] digest = sumHmac(signingKey, stringToSign.getBytes(StandardCharsets.UTF_8));
String signature = HexFormat.of().formatHex(digest);
return "%s%s?%s&%s=%s".formatted(endpoint, path, query, encodeS3Url("X-Amz-Signature"), encodeS3Url(signature));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private static String encodeS3Url(String input) {
return URLEncoder.encode(input, StandardCharsets.UTF_8)
.replace("!", "%21")
.replace("$", "%24")
.replace("&", "%26")
.replace("'", "%27")
.replace("(", "%28")
.replace(")", "%29")
.replace("*", "%2A")
.replace("+", "%2B")
.replace(",", "%2C")
.replace("/", "%2F")
.replace(":", "%3A")
.replace(";", "%3B")
.replace("=", "%3D")
.replace("@", "%40")
.replace("[", "%5B")
.replace("]", "%5D");
}
private static byte[] sumHmac(byte[] key, byte[] data) throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
mac.update(data);
return mac.doFinal();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment