Created
October 14, 2024 01:34
-
-
Save Eng-Fouad/e35959fbdf984df7a5d1fd3ec880fef1 to your computer and use it in GitHub Desktop.
Sign Minio/AWS S3 url manually in Java without dependencies
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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