io.minio.Signer.java Source code

Java tutorial

Introduction

Here is the source code for io.minio.Signer.java

Source

/*
 * Minio Java Library 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.minio;

import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.TreeMap;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.joda.time.DateTime;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.Headers;
import com.google.common.base.Joiner;
import com.google.common.io.BaseEncoding;
import com.google.common.net.UrlEscapers;
import com.google.common.escape.Escaper;

import io.minio.Digest;

/**
 * Amazon AWS S3 signature V4 signer.
 */
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.
    //
    //  Content-Length:
    //
    //      This is ignored from signing because generating a pre-signed URL should not provide a content-length
    //      constraint, specifically when vending a S3 pre-signed PUT URL. The corollary to this is that when
    //      sending regular requests (non-pre-signed), the signature contains a checksum of the body, which
    //      implicitly validates the payload length (since changing the number of bytes would change the checksum)
    //      and therefore this header is not valuable in the signature.
    //
    //  Content-Type:
    //
    //      Signing this header causes quite a number of problems in browser environments, where browsers
    //      like to modify and normalize the content-type header in different ways. There is more information
    //      on this in https://github.com/aws/aws-sdk-js/issues/244. Avoiding this field simplifies logic
    //      and reduces the possibility of future bugs
    //
    //  Authorization:
    //
    //      Is skipped for obvious reasons
    //
    private static final Escaper QUERY_ESCAPER = UrlEscapers.urlPathSegmentEscaper();
    private static final Set<String> IGNORED_HEADERS = new HashSet<>();

    static {
        IGNORED_HEADERS.add("authorization");
        IGNORED_HEADERS.add("content-type");
        IGNORED_HEADERS.add("content-length");
        IGNORED_HEADERS.add("user-agent");
    }

    private Request request;
    private String contentSha256;
    private DateTime date;
    private String region;
    private String accessKey;
    private String secretKey;

    private String scope;
    private Map<String, String> canonicalHeaders;
    private String signedHeaders;
    private HttpUrl 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.
     *
     */
    public Signer(Request request, String contentSha256, DateTime date, String region, String accessKey,
            String secretKey) {
        this.request = request;
        this.contentSha256 = contentSha256;
        this.date = date;
        this.region = region;
        this.accessKey = accessKey;
        this.secretKey = secretKey;
    }

    private void setScope() {
        this.scope = this.date.toString(DateFormat.SIGNER_DATE_FORMAT) + "/" + this.region + "/s3/aws4_request";
    }

    private void setCanonicalHeaders() {
        this.canonicalHeaders = new TreeMap<>();

        Headers headers = this.request.headers();
        for (String name : headers.names()) {
            String signedHeader = name.toLowerCase();
            if (!IGNORED_HEADERS.contains(signedHeader)) {
                this.canonicalHeaders.put(signedHeader, headers.get(name));
            }
        }

        this.signedHeaders = Joiner.on(";").join(this.canonicalHeaders.keySet());
    }

    private void setCanonicalQueryString() {
        Map<String, String> signedQueryParams = new TreeMap<>();

        String encodedQuery = this.url.encodedQuery();
        if (encodedQuery == null) {
            this.canonicalQueryString = "";
            return;
        }

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

    private void setCanonicalRequest() throws NoSuchAlgorithmException {
        setCanonicalHeaders();
        this.url = this.request.httpUrl();
        setCanonicalQueryString();

        // CanonicalRequest =
        //   HTTPRequestMethod + '\n' +
        //   CanonicalURI + '\n' +
        //   CanonicalQueryString + '\n' +
        //   CanonicalHeaders + '\n' +
        //   SignedHeaders + '\n' +
        //   HexEncode(Hash(RequestPayload))
        this.canonicalRequest = this.request.method() + "\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.toString(DateFormat.AMZ_DATE_FORMAT) + "\n"
                + this.scope + "\n" + this.canonicalRequestHash;
    }

    private void setSigningKey() throws NoSuchAlgorithmException, InvalidKeyException {
        String aws4SecretKey = "AWS4" + this.secretKey;

        byte[] dateKey = sumHmac(aws4SecretKey.getBytes(StandardCharsets.UTF_8),
                this.date.toString(DateFormat.SIGNER_DATE_FORMAT).getBytes(StandardCharsets.UTF_8));

        byte[] dateRegionKey = sumHmac(dateKey, this.region.getBytes(StandardCharsets.UTF_8));

        byte[] dateRegionServiceKey = sumHmac(dateRegionKey, "s3".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();
    }

    private void setAuthorization() {
        this.authorization = "AWS4-HMAC-SHA256 Credential=" + this.accessKey + "/" + this.scope + ", SignedHeaders="
                + this.signedHeaders + ", Signature=" + this.signature;
    }

    /**
     * Returns signed request object for given request, region, access key and secret key.
     */
    public static Request signV4(Request request, String region, String accessKey, String secretKey)
            throws NoSuchAlgorithmException, InvalidKeyException {
        String contentSha256 = request.header("x-amz-content-sha256");
        DateTime date = DateFormat.AMZ_DATE_FORMAT.parseDateTime(request.header("x-amz-date"));

        Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey);
        signer.setScope();
        signer.setCanonicalRequest();
        signer.setStringToSign();
        signer.setSigningKey();
        signer.setSignature();
        signer.setAuthorization();

        return request.newBuilder().header("Authorization", signer.authorization).build();
    }

    private void setPresignCanonicalRequest(int expires) throws NoSuchAlgorithmException {
        this.canonicalHeaders = new TreeMap<>();
        this.canonicalHeaders.put("host", this.request.headers().get("Host"));
        this.signedHeaders = "host";

        HttpUrl.Builder urlBuilder = this.request.httpUrl().newBuilder();
        // order of queryparam addition is important ie has to be sorted.
        urlBuilder.addEncodedQueryParameter(encodeQueryString("X-Amz-Algorithm"),
                encodeQueryString("AWS4-HMAC-SHA256"));
        urlBuilder.addEncodedQueryParameter(encodeQueryString("X-Amz-Credential"),
                encodeQueryString(this.accessKey + "/" + this.scope));
        urlBuilder.addEncodedQueryParameter(encodeQueryString("X-Amz-Date"),
                encodeQueryString(this.date.toString(DateFormat.AMZ_DATE_FORMAT)));
        urlBuilder.addEncodedQueryParameter(encodeQueryString("X-Amz-Expires"),
                encodeQueryString(Integer.toString(expires)));
        urlBuilder.addEncodedQueryParameter(encodeQueryString("X-Amz-SignedHeaders"),
                encodeQueryString(this.signedHeaders));
        this.url = urlBuilder.build();

        setCanonicalQueryString();

        this.canonicalRequest = this.request.method() + "\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 HttpUrl presignV4(Request request, String region, String accessKey, String secretKey, int expires)
            throws NoSuchAlgorithmException, InvalidKeyException {
        String contentSha256 = "UNSIGNED-PAYLOAD";
        DateTime date = DateFormat.AMZ_DATE_FORMAT.parseDateTime(request.header("x-amz-date"));

        Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey);
        signer.setScope();
        signer.setPresignCanonicalRequest(expires);
        signer.setStringToSign();
        signer.setSigningKey();
        signer.setSignature();

        return signer.url.newBuilder()
                .addEncodedQueryParameter(encodeQueryString("X-Amz-Signature"), encodeQueryString(signer.signature))
                .build();
    }

    /**
     * Returns credential string of given access key, date and region.
     */
    public static String credential(String accessKey, DateTime date, String region) {
        return accessKey + "/" + date.toString(DateFormat.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, DateTime date, String region)
            throws NoSuchAlgorithmException, InvalidKeyException {
        Signer signer = new Signer(null, null, date, region, null, secretKey);
        signer.stringToSign = stringToSign;
        signer.setSigningKey();
        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();
    }

    /**
     * Returns encoded query string for URL.
     */
    public static String encodeQueryString(String str) {
        if (str == null) {
            return "";
        }

        return QUERY_ESCAPER.escape(str).replaceAll("\\!", "%21").replaceAll("\\$", "%24").replaceAll("\\&", "%26")
                .replaceAll("\\'", "%27").replaceAll("\\(", "%28").replaceAll("\\)", "%29").replaceAll("\\*", "%2A")
                .replaceAll("\\+", "%2B").replaceAll("\\,", "%2C").replaceAll("\\\\", "%2F")
                .replaceAll("\\:", "%3A").replaceAll("\\;", "%3B").replaceAll("\\=", "%3D").replaceAll("\\@", "%40")
                .replaceAll("\\[", "%5B").replaceAll("\\]", "%5D");
    }
}