diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..b74f3478 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1 @@ +This product includes software developed by Minio. (https://github.com/minio/minio-java) diff --git a/pom.xml b/pom.xml index 81585780..a01a63f7 100644 --- a/pom.xml +++ b/pom.xml @@ -40,11 +40,13 @@ 22.0.0 ${project.basedir} - 8 true true + 8 + true 245 + 2.25.32 @@ -56,6 +58,40 @@ pom import + + + software.amazon.awssdk + bom + ${dep.aws-sdk.version} + pom + import + + + + + + com.mycila + license-maven-plugin + + + + + **/io/trino/s3/proxy/server/minio/* + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + **/io/trino/s3/proxy/server/minio/* + + + + diff --git a/trino-s3-proxy/pom.xml b/trino-s3-proxy/pom.xml index 28d9ab81..ecae50e8 100644 --- a/trino-s3-proxy/pom.xml +++ b/trino-s3-proxy/pom.xml @@ -64,6 +64,12 @@ jakarta.ws.rs-api + + org.assertj + assertj-core + test + + org.junit.jupiter junit-jupiter-api diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/Credentials.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/Credentials.java new file mode 100644 index 00000000..11d093e6 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/Credentials.java @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.s3.proxy.server; + +import static java.util.Objects.requireNonNull; + +public record Credentials(String emulatedAccessKey, String emulatedSecretKey) +{ + public Credentials + { + requireNonNull(emulatedAccessKey, "emulatedAccessKey is null"); + requireNonNull(emulatedSecretKey, "emulatedSecretKey is null"); + } +} diff --git a/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/DummyTest.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/CredentialsController.java similarity index 81% rename from trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/DummyTest.java rename to trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/CredentialsController.java index 00ca9ac6..f763fa54 100644 --- a/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/DummyTest.java +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/CredentialsController.java @@ -13,13 +13,9 @@ */ package io.trino.s3.proxy.server; -import org.junit.jupiter.api.Test; +import java.util.Optional; -public class DummyTest +public interface CredentialsController { - @Test - public void testDummy() - { - // stub test for now - } + Optional credentials(String emulatedAccessKey); } diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/SigningController.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/SigningController.java new file mode 100644 index 00000000..8cd239e3 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/SigningController.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.s3.proxy.server; + +import com.google.inject.Inject; +import io.trino.s3.proxy.server.minio.Signer; +import io.trino.s3.proxy.server.minio.emulation.MinioRequest; +import io.trino.s3.proxy.server.minio.emulation.MinioUrl; +import jakarta.ws.rs.core.MultivaluedMap; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class SigningController +{ + private final CredentialsController credentialsController; + + @Inject + public SigningController(CredentialsController credentialsController) + { + this.credentialsController = requireNonNull(credentialsController, "credentialsController is null"); + } + + public Map signedRequestHeaders(String method, MultivaluedMap requestHeaders, String encodedPath, String encodedQuery, String region, String accessKey) + { + // TODO + Credentials credentials = credentialsController.credentials(accessKey).orElseThrow(); + + MinioUrl minioUrl = MinioUrl.build(encodedPath, encodedQuery); + MinioRequest minioRequest = MinioRequest.build(requestHeaders, method, minioUrl); + + // TODO + String sha256 = minioRequest.headerValue("x-amz-content-sha256").orElseThrow(); + + try { + return Signer.signV4S3(minioRequest, region, accessKey, credentials.emulatedSecretKey(), sha256).headers(); + } + catch (NoSuchAlgorithmException | InvalidKeyException e) { + // TODO + throw new RuntimeException(e); + } + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Digest.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Digest.java new file mode 100644 index 00000000..d77a5265 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Digest.java @@ -0,0 +1,155 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.trino.s3.proxy.server.minio; + +import com.google.common.io.BaseEncoding; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Locale; + +/** Various global static functions used. */ +public class Digest { + // MD5 hash of zero length byte array. + public static final String ZERO_MD5_HASH = "1B2M2Y8AsgTpgAmY7PhCfg=="; + // SHA-256 hash of zero length byte array. + public static final String ZERO_SHA256_HASH = + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + + /** Private constructor. */ + private Digest() {} + + /** Returns MD5 hash of byte array. */ + public static String md5Hash(byte[] data, int length) throws NoSuchAlgorithmException { + MessageDigest md5Digest = MessageDigest.getInstance("MD5"); + md5Digest.update(data, 0, length); + return Base64.getEncoder().encodeToString(md5Digest.digest()); + } + + /** Returns SHA-256 hash of byte array. */ + public static String sha256Hash(byte[] data, int length) throws NoSuchAlgorithmException { + MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256"); + sha256Digest.update((byte[]) data, 0, length); + return BaseEncoding.base16().encode(sha256Digest.digest()).toLowerCase(Locale.US); + } + + /** Returns SHA-256 hash of given string. */ + public static String sha256Hash(String string) throws NoSuchAlgorithmException { + byte[] data = string.getBytes(StandardCharsets.UTF_8); + return sha256Hash(data, data.length); + } + + /** + * Returns SHA-256 and MD5 hashes of given data and it's length. + * + * @param data must be {@link RandomAccessFile}, {@link BufferedInputStream} or byte array. + * @param len length of data to be read for hash calculation. + * @deprecated This method is no longer supported. + */ + @Deprecated + public static String[] sha256Md5Hashes(Object data, int len) + throws NoSuchAlgorithmException, IOException, InsufficientDataException, InternalException { + MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256"); + MessageDigest md5Digest = MessageDigest.getInstance("MD5"); + + if (data instanceof BufferedInputStream || data instanceof RandomAccessFile) { + updateDigests(data, len, sha256Digest, md5Digest); + } else if (data instanceof byte[]) { + sha256Digest.update((byte[]) data, 0, len); + md5Digest.update((byte[]) data, 0, len); + } else { + throw new InternalException( + "Unknown data source to calculate SHA-256 hash. This should not happen, " + + "please report this issue at https://github.com/minio/minio-java/issues", + null); + } + + return new String[] { + BaseEncoding.base16().encode(sha256Digest.digest()).toLowerCase(Locale.US), + BaseEncoding.base64().encode(md5Digest.digest()) + }; + } + + /** Updated MessageDigest with bytes read from file and stream. */ + private static int updateDigests( + Object inputStream, int len, MessageDigest sha256Digest, MessageDigest md5Digest) + throws IOException, InsufficientDataException { + RandomAccessFile file = null; + BufferedInputStream stream = null; + if (inputStream instanceof RandomAccessFile) { + file = (RandomAccessFile) inputStream; + } else if (inputStream instanceof BufferedInputStream) { + stream = (BufferedInputStream) inputStream; + } + + // hold current position of file/stream to reset back to this position. + long pos = 0; + if (file != null) { + pos = file.getFilePointer(); + } else { + stream.mark(len); + } + + // 16KiB buffer for optimization + byte[] buf = new byte[16384]; + int bytesToRead = buf.length; + int bytesRead = 0; + int totalBytesRead = 0; + while (totalBytesRead < len) { + if ((len - totalBytesRead) < bytesToRead) { + bytesToRead = len - totalBytesRead; + } + + if (file != null) { + bytesRead = file.read(buf, 0, bytesToRead); + } else { + bytesRead = stream.read(buf, 0, bytesToRead); + } + + if (bytesRead < 0) { + // reached EOF + throw new InsufficientDataException( + "Insufficient data. bytes read " + totalBytesRead + " expected " + len); + } + + if (bytesRead > 0) { + if (sha256Digest != null) { + sha256Digest.update(buf, 0, bytesRead); + } + + if (md5Digest != null) { + md5Digest.update(buf, 0, bytesRead); + } + + totalBytesRead += bytesRead; + } + } + + // reset back to saved position. + if (file != null) { + file.seek(pos); + } else { + stream.reset(); + } + + return totalBytesRead; + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InsufficientDataException.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InsufficientDataException.java new file mode 100644 index 00000000..8adc4e26 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InsufficientDataException.java @@ -0,0 +1,29 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.trino.s3.proxy.server.minio; + +/** + * Thrown to indicate that reading given InputStream gets EOFException before reading given length. + */ +public class InsufficientDataException extends MinioException { + private static final long serialVersionUID = -1619719290805056566L; + + /** Constructs a new InsufficientDataException with given error message. */ + public InsufficientDataException(String message) { + super(message); + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InternalException.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InternalException.java new file mode 100644 index 00000000..2049718c --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/InternalException.java @@ -0,0 +1,29 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.trino.s3.proxy.server.minio; + +/** + * Thrown to indicate that unexpected internal library error occured while processing given request. + */ +public class InternalException extends MinioException { + private static final long serialVersionUID = 138336287983212416L; + + /** Constructs a new InternalException with given error message. */ + public InternalException(String message, String httpTrace) { + super(message, httpTrace); + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/MinioException.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/MinioException.java new file mode 100644 index 00000000..af6eb545 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/MinioException.java @@ -0,0 +1,44 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.trino.s3.proxy.server.minio; + +/** Base Exception class for all minio-java exceptions. */ +public class MinioException extends Exception { + private static final long serialVersionUID = -7241010318779326306L; + + String httpTrace = null; + + /** Constructs a new MinioException. */ + public MinioException() { + super(); + } + + /** Constructs a new MinioException with given error message. */ + public MinioException(String message) { + super(message); + } + + /** Constructs a new MinioException with given error message. */ + public MinioException(String message, String httpTrace) { + super(message); + this.httpTrace = httpTrace; + } + + public String httpTrace() { + return this.httpTrace; + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/S3Escaper.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/S3Escaper.java new file mode 100644 index 00000000..bb3e1123 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/S3Escaper.java @@ -0,0 +1,110 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2016 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.trino.s3.proxy.server.minio; + +import com.google.common.escape.Escaper; +import com.google.common.net.UrlEscapers; + +public class S3Escaper { + private static final Escaper ESCAPER = UrlEscapers.urlPathSegmentEscaper(); + + /** Returns S3 encoded string. */ + public static String encode(String str) { + if (str == null) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + for (char ch : ESCAPER.escape(str).toCharArray()) { + switch (ch) { + case '!': + builder.append("%21"); + break; + case '$': + builder.append("%24"); + break; + case '&': + builder.append("%26"); + break; + case '\'': + builder.append("%27"); + break; + case '(': + builder.append("%28"); + break; + case ')': + builder.append("%29"); + break; + case '*': + builder.append("%2A"); + break; + case '+': + builder.append("%2B"); + break; + case ',': + builder.append("%2C"); + break; + case '/': + builder.append("%2F"); + break; + case ':': + builder.append("%3A"); + break; + case ';': + builder.append("%3B"); + break; + case '=': + builder.append("%3D"); + break; + case '@': + builder.append("%40"); + break; + case '[': + builder.append("%5B"); + break; + case ']': + builder.append("%5D"); + break; + default: + builder.append(ch); + } + } + return builder.toString(); + } + + /** Returns S3 encoded string of given path where multiple '/' are trimmed. */ + public static String encodePath(String path) { + final StringBuilder encodedPath = new StringBuilder(); + for (String pathSegment : path.split("/")) { + if (!pathSegment.isEmpty()) { + if (encodedPath.length() > 0) { + encodedPath.append("/"); + } + encodedPath.append(S3Escaper.encode(pathSegment)); + } + } + + if (path.startsWith("/")) { + encodedPath.insert(0, "/"); + } + if (path.endsWith("/")) { + encodedPath.append("/"); + } + + return encodedPath.toString(); + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Signer.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Signer.java new file mode 100644 index 00000000..e721a762 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Signer.java @@ -0,0 +1,444 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2015 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.trino.s3.proxy.server.minio; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.io.BaseEncoding; +import io.trino.s3.proxy.server.minio.emulation.MinioHeaders; +import io.trino.s3.proxy.server.minio.emulation.MinioUrl; +import io.trino.s3.proxy.server.minio.emulation.MinioRequest; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * Amazon AWS S3 signature V4 signer. + */ +public class Signer +{ + // + // Excerpts from @lsegal - https://github.com/aws/aws-sdk-js/issues/659#issuecomment-120477258 + // + // * User-Agent + // This is ignored from signing because signing this causes problems with generating pre-signed + // URLs (that are executed by other agents) or when customers pass requests through proxies, which + // may modify the user-agent. + // + // * Authorization + // Is skipped for obvious reasons. + // + // * Accept-Encoding + // Some S3 servers like Hitachi Content Platform do not honour this header for signature + // calculation. + // + private static final Set IGNORED_HEADERS = + ImmutableSet.of("accept-encoding", "authorization", "user-agent"); + private static final Set PRESIGN_IGNORED_HEADERS = + ImmutableSet.of( + "accept-encoding", + "authorization", + "user-agent", + "content-md5", + "x-amz-content-sha256", + "x-amz-date", + "x-amz-security-token"); + + private MinioRequest request; + private String contentSha256; + private ZonedDateTime date; + private String region; + private String accessKey; + private String secretKey; + private String prevSignature; + + private String scope; + private Map canonicalHeaders; + private String signedHeaders; + private MinioUrl url; + private String canonicalQueryString; + private String canonicalRequest; + private String canonicalRequestHash; + private String stringToSign; + private byte[] signingKey; + private String signature; + private String authorization; + + /** + * Create new Signer object for V4. + * + * @param request HTTP Request object. + * @param contentSha256 SHA-256 hash of request payload. + * @param date Date to be used to sign the request. + * @param region Amazon AWS region for the request. + * @param accessKey Access Key string. + * @param secretKey Secret Key string. + * @param prevSignature Previous signature of chunk upload. + */ + private Signer( + MinioRequest request, + String contentSha256, + ZonedDateTime date, + String region, + String accessKey, + String secretKey, + String prevSignature) + { + this.request = request; + this.contentSha256 = contentSha256; + this.date = date; + this.region = region; + this.accessKey = accessKey; + this.secretKey = secretKey; + this.prevSignature = prevSignature; + } + + private void setScope(String serviceName) + { + this.scope = + this.date.format(Time.SIGNER_DATE_FORMAT) + + "/" + + this.region + + "/" + + serviceName + + "/aws4_request"; + } + + private void setCanonicalHeaders(Set ignored_headers) + { + this.canonicalHeaders = new TreeMap<>(); + + for (String name : request.headerNames()) { + String signedHeader = name.toLowerCase(Locale.US); + if (!ignored_headers.contains(signedHeader)) { + // Convert and add header values as per + // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html + // * Header having multiple values should be converted to comma separated values. + // * Multi-spaced value of header should be trimmed to single spaced value. + this.canonicalHeaders.put( + signedHeader, + request.headerValues(name).stream() + .map( + value -> { + return value.replaceAll("( +)", " "); + }) + .collect(Collectors.joining(","))); + } + } + + this.signedHeaders = Joiner.on(";").join(this.canonicalHeaders.keySet()); + } + + private void setCanonicalQueryString() + { + String encodedQuery = this.url.encodedQuery(); + if (encodedQuery.isEmpty()) { + this.canonicalQueryString = ""; + return; + } + + // Building a multimap which only order keys, ordering values is not performed + // until MinIO server supports it. + Multimap signedQueryParams = + MultimapBuilder.treeKeys().arrayListValues().build(); + + for (String queryParam : encodedQuery.split("&")) { + String[] tokens = queryParam.split("="); + if (tokens.length > 1) { + signedQueryParams.put(tokens[0], tokens[1]); + } + else { + signedQueryParams.put(tokens[0], ""); + } + } + + this.canonicalQueryString = + Joiner.on("&").withKeyValueSeparator("=").join(signedQueryParams.entries()); + } + + private void setCanonicalRequest() + throws NoSuchAlgorithmException + { + setCanonicalHeaders(IGNORED_HEADERS); + this.url = this.request.url(); + setCanonicalQueryString(); + + // CanonicalRequest = + // HTTPRequestMethod + '\n' + + // CanonicalURI + '\n' + + // CanonicalQueryString + '\n' + + // CanonicalHeaders + '\n' + + // SignedHeaders + '\n' + + // HexEncode(Hash(RequestPayload)) + this.canonicalRequest = + this.request.httpMethod() + + "\n" + + this.url.encodedPath() + + "\n" + + this.canonicalQueryString + + "\n" + + Joiner.on("\n").withKeyValueSeparator(":").join(this.canonicalHeaders) + + "\n\n" + + this.signedHeaders + + "\n" + + this.contentSha256; + + this.canonicalRequestHash = Digest.sha256Hash(this.canonicalRequest); + } + + private void setStringToSign() + { + this.stringToSign = + "AWS4-HMAC-SHA256" + + "\n" + + this.date.format(Time.AMZ_DATE_FORMAT) + + "\n" + + this.scope + + "\n" + + this.canonicalRequestHash; + } + + private void setChunkStringToSign() + throws NoSuchAlgorithmException + { + this.stringToSign = + "AWS4-HMAC-SHA256-PAYLOAD" + + "\n" + + this.date.format(Time.AMZ_DATE_FORMAT) + + "\n" + + this.scope + + "\n" + + this.prevSignature + + "\n" + + Digest.sha256Hash("") + + "\n" + + this.contentSha256; + } + + private void setSigningKey(String serviceName) + throws NoSuchAlgorithmException, InvalidKeyException + { + String aws4SecretKey = "AWS4" + this.secretKey; + + byte[] dateKey = + sumHmac( + aws4SecretKey.getBytes(StandardCharsets.UTF_8), + this.date.format(Time.SIGNER_DATE_FORMAT).getBytes(StandardCharsets.UTF_8)); + + byte[] dateRegionKey = sumHmac(dateKey, this.region.getBytes(StandardCharsets.UTF_8)); + + byte[] dateRegionServiceKey = + sumHmac(dateRegionKey, serviceName.getBytes(StandardCharsets.UTF_8)); + + this.signingKey = + sumHmac(dateRegionServiceKey, "aws4_request".getBytes(StandardCharsets.UTF_8)); + } + + private void setSignature() + throws NoSuchAlgorithmException, InvalidKeyException + { + byte[] digest = sumHmac(this.signingKey, this.stringToSign.getBytes(StandardCharsets.UTF_8)); + this.signature = BaseEncoding.base16().encode(digest).toLowerCase(Locale.US); + } + + private void setAuthorization() + { + this.authorization = + "AWS4-HMAC-SHA256 Credential=" + + this.accessKey + + "/" + + this.scope + + ", SignedHeaders=" + + this.signedHeaders + + ", Signature=" + + this.signature; + } + + /** + * Returns chunk signature calculated using given arguments. + */ + public static String getChunkSignature( + String chunkSha256, ZonedDateTime date, String region, String secretKey, String prevSignature) + throws NoSuchAlgorithmException, InvalidKeyException + { + Signer signer = new Signer(null, chunkSha256, date, region, null, secretKey, prevSignature); + signer.setScope("s3"); + signer.setChunkStringToSign(); + signer.setSigningKey("s3"); + signer.setSignature(); + + return signer.signature; + } + + /** + * Returns signed request object for given request, region, access key and secret key. + */ + private static MinioHeaders signV4( + String serviceName, + MinioRequest request, + String region, + String accessKey, + String secretKey, + String contentSha256) + throws NoSuchAlgorithmException, InvalidKeyException + { + ZonedDateTime date = ZonedDateTime.parse(request.headerValue("x-amz-date").orElseThrow(), Time.AMZ_DATE_FORMAT); + + Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null); + signer.setScope(serviceName); + signer.setCanonicalRequest(); + signer.setStringToSign(); + signer.setSigningKey(serviceName); + signer.setSignature(); + signer.setAuthorization(); + + return MinioHeaders.of("Authorization", signer.authorization); + } + + /** + * Returns signed request of given request for S3 service. + */ + public static MinioHeaders signV4S3( + MinioRequest request, String region, String accessKey, String secretKey, String contentSha256) + throws NoSuchAlgorithmException, InvalidKeyException + { + return signV4("s3", request, region, accessKey, secretKey, contentSha256); + } + + /** + * Returns signed request of given request for STS service. + */ + public static MinioHeaders signV4Sts( + MinioRequest request, String region, String accessKey, String secretKey, String contentSha256) + throws NoSuchAlgorithmException, InvalidKeyException + { + return signV4("sts", request, region, accessKey, secretKey, contentSha256); + } + + private void setPresignCanonicalRequest(int expires) + throws NoSuchAlgorithmException + { + setCanonicalHeaders(PRESIGN_IGNORED_HEADERS); + + List parts = new ArrayList<>(); + parts.add(String.join("=", S3Escaper.encode("X-Amz-Algorithm"), S3Escaper.encode("AWS4-HMAC-SHA256"))); + parts.add(String.join("=", S3Escaper.encode("X-Amz-Credential"), S3Escaper.encode(this.accessKey + "/" + this.scope))); + parts.add(String.join("=", S3Escaper.encode("X-Amz-Date"), S3Escaper.encode(this.date.format(Time.AMZ_DATE_FORMAT)))); + parts.add(String.join("=", S3Escaper.encode("X-Amz-Expires"), S3Escaper.encode(Integer.toString(expires)))); + parts.add(String.join("=", S3Escaper.encode("X-Amz-SignedHeaders"), S3Escaper.encode(this.signedHeaders))); + String query = String.join("&", parts); + this.url = this.url.appendQuery(query); + + setCanonicalQueryString(); + + this.canonicalRequest = + this.request.httpMethod() + + "\n" + + this.url.encodedPath() + + "\n" + + this.canonicalQueryString + + "\n" + + Joiner.on("\n").withKeyValueSeparator(":").join(this.canonicalHeaders) + + "\n\n" + + this.signedHeaders + + "\n" + + this.contentSha256; + + this.canonicalRequestHash = Digest.sha256Hash(this.canonicalRequest); + } + + /** + * Returns pre-signed HttpUrl object for given request, region, access key, secret key and expires + * time. + */ + public static MinioUrl presignV4( + MinioRequest request, String region, String accessKey, String secretKey, int expires) + throws NoSuchAlgorithmException, InvalidKeyException + { + String contentSha256 = "UNSIGNED-PAYLOAD"; + ZonedDateTime date = ZonedDateTime.parse(request.headerValue("x-amz-date").orElseThrow(), Time.AMZ_DATE_FORMAT); + + Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null); + signer.setScope("s3"); + signer.setPresignCanonicalRequest(expires); + signer.setStringToSign(); + signer.setSigningKey("s3"); + signer.setSignature(); + + String query = String.join("=", S3Escaper.encode("X-Amz-Signature"), S3Escaper.encode(signer.signature)); + + return signer + .url + .appendQuery(query); + } + + /** + * Returns credential string of given access key, date and region. + */ + public static String credential(String accessKey, ZonedDateTime date, String region) + { + return accessKey + + "/" + + date.format(Time.SIGNER_DATE_FORMAT) + + "/" + + region + + "/s3/aws4_request"; + } + + /** + * Returns pre-signed post policy string for given stringToSign, secret key, date and region. + */ + public static String postPresignV4( + String stringToSign, String secretKey, ZonedDateTime date, String region) + throws NoSuchAlgorithmException, InvalidKeyException + { + Signer signer = new Signer(null, null, date, region, null, secretKey, null); + signer.stringToSign = stringToSign; + signer.setSigningKey("s3"); + signer.setSignature(); + + return signer.signature; + } + + /** + * Returns HMacSHA256 digest of given key and data. + */ + public 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(); + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Time.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Time.java new file mode 100644 index 00000000..a977f782 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/Time.java @@ -0,0 +1,46 @@ +/* + * MinIO Java SDK for Amazon S3 Compatible Cloud Storage, + * (C) 2020 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.trino.s3.proxy.server.minio; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** Time formatters for S3 APIs. */ +public class Time { + public static final ZoneId UTC = ZoneId.of("Z"); + + public static final DateTimeFormatter AMZ_DATE_FORMAT = + DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US).withZone(UTC); + + public static final DateTimeFormatter RESPONSE_DATE_FORMAT = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH':'mm':'ss'.'SSS'Z'", Locale.US).withZone(UTC); + + // Formatted string is convertible to LocalDate only, not to LocalDateTime or ZonedDateTime. + // Below example shows how to use this to get ZonedDateTime. + // LocalDate.parse("20200225", SIGNER_DATE_FORMAT).atStartOfDay(UTC); + public static final DateTimeFormatter SIGNER_DATE_FORMAT = + DateTimeFormatter.ofPattern("yyyyMMdd", Locale.US).withZone(UTC); + + public static final DateTimeFormatter HTTP_HEADER_DATE_FORMAT = + DateTimeFormatter.ofPattern("EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'", Locale.US).withZone(UTC); + + public static final DateTimeFormatter EXPIRATION_DATE_FORMAT = RESPONSE_DATE_FORMAT; + + private Time() {} +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioHeaders.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioHeaders.java new file mode 100644 index 00000000..fd2db5b7 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioHeaders.java @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.s3.proxy.server.minio.emulation; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public record MinioHeaders(Map headers) +{ + public MinioHeaders + { + headers = ImmutableMap.copyOf(headers); + } + + public static MinioHeaders of(String name, String value) + { + return new MinioHeaders(ImmutableMap.of(name, value)); + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequest.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequest.java new file mode 100644 index 00000000..9741457a --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequest.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.s3.proxy.server.minio.emulation; + +import jakarta.ws.rs.core.MultivaluedMap; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +public interface MinioRequest +{ + Collection headerNames(); + + List headerValues(String name); + + Optional headerValue(String name); + + String httpMethod(); + + MinioUrl url(); + + static MinioRequest build(MultivaluedMap headers, String httpMethod, MinioUrl url) + { + return new MinioRequestImpl(headers, httpMethod, url); + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequestImpl.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequestImpl.java new file mode 100644 index 00000000..85bc4b46 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioRequestImpl.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.s3.proxy.server.minio.emulation; + +import com.google.common.collect.ImmutableList; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +class MinioRequestImpl + implements MinioRequest +{ + private final MultivaluedMap headers; + private final String httpMethod; + private final MinioUrl url; + + MinioRequestImpl(MultivaluedMap headers, String httpMethod, MinioUrl url) + { + requireNonNull(headers, "headers is null"); + this.httpMethod = requireNonNull(httpMethod, "httpMethod is null"); + this.url = requireNonNull(url, "url is null"); + + this.headers = new MultivaluedHashMap<>(); + headers.forEach((key, values) -> this.headers.put(key.toLowerCase(Locale.ROOT), values)); + } + + @Override + public Collection headerNames() + { + return headers.keySet(); + } + + @Override + public List headerValues(String name) + { + return headers.getOrDefault(name, ImmutableList.of()); + } + + @Override + public Optional headerValue(String name) + { + return Optional.ofNullable(headers.getFirst(name)); + } + + @Override + public String httpMethod() + { + return httpMethod; + } + + @Override + public MinioUrl url() + { + return url; + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrl.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrl.java new file mode 100644 index 00000000..f22a9c48 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrl.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.s3.proxy.server.minio.emulation; + +public interface MinioUrl +{ + String encodedQuery(); + + String encodedPath(); + + MinioUrl appendQuery(String query); + + static MinioUrl build(String encodedPath, String encodedQuery) + { + return new MinioUrlImpl(encodedPath, encodedQuery); + } +} diff --git a/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrlImpl.java b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrlImpl.java new file mode 100644 index 00000000..5cc78ab2 --- /dev/null +++ b/trino-s3-proxy/src/main/java/io/trino/s3/proxy/server/minio/emulation/MinioUrlImpl.java @@ -0,0 +1,47 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.s3.proxy.server.minio.emulation; + +import static java.util.Objects.requireNonNull; + +class MinioUrlImpl + implements MinioUrl +{ + private final String encodedPath; + private final String encodedQuery; + + MinioUrlImpl(String encodedPath, String encodedQuery) + { + this.encodedPath = requireNonNull(encodedPath, "encodedPath is null"); + this.encodedQuery = requireNonNull(encodedQuery, "encodedQuery is null"); + } + + @Override + public String encodedQuery() + { + return encodedQuery; + } + + @Override + public String encodedPath() + { + return encodedPath; + } + + @Override + public MinioUrl appendQuery(String query) + { + return new MinioUrlImpl(encodedPath, encodedQuery.isEmpty() ? query : (encodedPath + "&" + query)); + } +} diff --git a/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/TestSigningController.java b/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/TestSigningController.java new file mode 100644 index 00000000..87d330a1 --- /dev/null +++ b/trino-s3-proxy/src/test/java/io/trino/s3/proxy/server/TestSigningController.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.s3.proxy.server; + +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestSigningController +{ + private static final Credentials CREDENTIALS = new Credentials("THIS_IS_AN_ACCESS_KEY", "THIS_IS_A_SECRET_KEY"); + + @Test + public void testRootLs() + { + CredentialsController credentialsController = accessKey -> Optional.of(CREDENTIALS); + SigningController signingController = new SigningController(credentialsController); + + // values discovered from an AWS CLI request sent to a dummy local HTTP server + MultivaluedMap requestHeaders = new MultivaluedHashMap<>(); + requestHeaders.putSingle("X-Amz-Date", "20240516T024511Z"); + requestHeaders.putSingle("X-Amz-Content-SHA256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + requestHeaders.putSingle("X-Amz-Security-Token", "FwoGZXIvYXdzEP3//////////wEaDG79rlcAjsgKPP9N3SKIAu7/Zvngne5Ov6kGrDcIIPUZYkGpwNbj8zNnbWgOhiqmOCM3hrk4NuH17mP5n3nC7urlXZxaTCywKpAHpO3YsvLXcwjlfaYFA0Au4oejwSbU9ybIlzPzrqz7lVesgCfJOV+rj5F5UAh19d7RpRpA6Vy4nxGBTTlCNIVbkW9fp2Esql2/vsdh77rAG+j+BQegtegDCKBfen4gHMdvEOF6hyc4ne43eLXjpvUKxBgpI9MjOHtNHrDbOOBFXDDyknoESgE9Hsm12nDuVQhwrI/hhA4YB/MSIpl4FTgVs2sQP3K+v65tmyvIlpL6O78S6spMM9Tv/F4JLtksTzb90w46uZk9sxKC/RBkRijisM6tBjIrr/0znxnW3i5ggGAX4H/Z3aWlxSdzNs2UGWtqig9Plp3Xa9gG+zCKcXmDAA=="); + requestHeaders.putSingle("Host", "localhost:10064"); + requestHeaders.putSingle("User-Agent", "aws-cli/2.15.16 Python/3.11.7 Darwin/22.6.0 source/x86_64 prompt/off command/s3.ls"); + requestHeaders.putSingle("Accept-Encoding", "identity"); + + Map signedHeaders = signingController.signedRequestHeaders("GET", requestHeaders, "/", "", "us-east-1", "THIS_IS_AN_ACCESS_KEY"); + + assertThat(signedHeaders).contains(Map.entry("Authorization", "AWS4-HMAC-SHA256 Credential=THIS_IS_AN_ACCESS_KEY/20240516/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=9a19c251bf4e1533174e80da59fa57c65b3149b611ec9a4104f6944767c25704")); + } + + @Test + public void testBucketLs() + { + CredentialsController credentialsController = accessKey -> Optional.of(CREDENTIALS); + SigningController signingController = new SigningController(credentialsController); + + // values discovered from an AWS CLI request sent to a dummy local HTTP server + MultivaluedMap requestHeaders = new MultivaluedHashMap<>(); + requestHeaders.putSingle("X-Amz-Date", "20240516T034003Z"); + requestHeaders.putSingle("X-Amz-Content-SHA256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); + requestHeaders.putSingle("X-Amz-Security-Token", "FwoGZXIvYXdzEP3//////////wEaDG79rlcAjsgKPP9N3SKIAu7/Zvngne5Ov6kGrDcIIPUZYkGpwNbj8zNnbWgOhiqmOCM3hrk4NuH17mP5n3nC7urlXZxaTCywKpAHpO3YsvLXcwjlfaYFA0Au4oejwSbU9ybIlzPzrqz7lVesgCfJOV+rj5F5UAh19d7RpRpA6Vy4nxGBTTlCNIVbkW9fp2Esql2/vsdh77rAG+j+BQegtegDCKBfen4gHMdvEOF6hyc4ne43eLXjpvUKxBgpI9MjOHtNHrDbOOBFXDDyknoESgE9Hsm12nDuVQhwrI/hhA4YB/MSIpl4FTgVs2sQP3K+v65tmyvIlpL6O78S6spMM9Tv/F4JLtksTzb90w46uZk9sxKC/RBkRijisM6tBjIrr/0znxnW3i5ggGAX4H/Z3aWlxSdzNs2UGWtqig9Plp3Xa9gG+zCKcXmDAA=="); + requestHeaders.putSingle("Host", "localhost:10064"); + requestHeaders.putSingle("User-Agent", "aws-cli/2.15.16 Python/3.11.7 Darwin/22.6.0 source/x86_64 prompt/off command/s3.ls"); + requestHeaders.putSingle("Accept-Encoding", "identity"); + + Map signedHeaders = signingController.signedRequestHeaders("GET", requestHeaders, "/mybucket", "list-type=2&prefix=foo%2Fbar&delimiter=%2F&encoding-type=url", "us-east-1", "THIS_IS_AN_ACCESS_KEY"); + + assertThat(signedHeaders).contains(Map.entry("Authorization", "AWS4-HMAC-SHA256 Credential=THIS_IS_AN_ACCESS_KEY/20240516/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=222d7b7fcd4d5560c944e8fecd9424ee3915d131c3ad9e000d65db93e87946c4")); + } +}