org.gaul.s3proxy.AwsSignature.java Source code

Java tutorial

Introduction

Here is the source code for org.gaul.s3proxy.AwsSignature.java

Source

/*
 * Copyright 2014-2017 Andrew Gaul <andrew@gaul.org>
 *
 * 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 org.gaul.s3proxy;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;

import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.common.io.BaseEncoding;
import com.google.common.net.HttpHeaders;
import com.google.common.net.PercentEscaper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class AwsSignature {
    private static final Logger logger = LoggerFactory.getLogger(S3ProxyHandler.class);
    private static final PercentEscaper AWS_URL_PARAMETER_ESCAPER = new PercentEscaper("-_.~", false);
    private static final Set<String> SIGNED_SUBRESOURCES = ImmutableSet.of("acl", "delete", "lifecycle", "location",
            "logging", "notification", "partNumber", "policy", "requestPayment", "response-cache-control",
            "response-content-disposition", "response-content-encoding", "response-content-language",
            "response-content-type", "response-expires", "torrent", "uploadId", "uploads", "versionId",
            "versioning", "versions", "website");

    private AwsSignature() {
    }

    /**
     * Create Amazon V2 signature.  Reference:
     * http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html
     */
    static String createAuthorizationSignature(HttpServletRequest request, String uri, String credential) {
        // sort Amazon headers
        SortedSetMultimap<String, String> canonicalizedHeaders = TreeMultimap.create();
        for (String headerName : Collections.list(request.getHeaderNames())) {
            Collection<String> headerValues = Collections.list(request.getHeaders(headerName));
            headerName = headerName.toLowerCase();
            if (!headerName.startsWith("x-amz-")) {
                continue;
            }
            if (headerValues.isEmpty()) {
                canonicalizedHeaders.put(headerName, "");
            }
            for (String headerValue : headerValues) {
                canonicalizedHeaders.put(headerName, Strings.nullToEmpty(headerValue));
            }
        }

        // build string to sign
        StringBuilder builder = new StringBuilder().append(request.getMethod()).append('\n')
                .append(Strings.nullToEmpty(request.getHeader(HttpHeaders.CONTENT_MD5))).append('\n')
                .append(Strings.nullToEmpty(request.getHeader(HttpHeaders.CONTENT_TYPE))).append('\n');
        String expires = request.getParameter("Expires");
        if (expires != null) {
            builder.append(expires);
        } else if (!canonicalizedHeaders.containsKey("x-amz-date")) {
            builder.append(request.getHeader(HttpHeaders.DATE));
        }
        builder.append('\n');
        for (Map.Entry<String, String> entry : canonicalizedHeaders.entries()) {
            builder.append(entry.getKey()).append(':').append(entry.getValue()).append('\n');
        }
        builder.append(uri);

        char separator = '?';
        List<String> subresources = Collections.list(request.getParameterNames());
        Collections.sort(subresources);
        for (String subresource : subresources) {
            if (SIGNED_SUBRESOURCES.contains(subresource)) {
                builder.append(separator).append(subresource);

                String value = request.getParameter(subresource);
                if (!"".equals(value)) {
                    builder.append('=').append(value);
                }
                separator = '&';
            }
        }

        String stringToSign = builder.toString();
        logger.trace("stringToSign: {}", stringToSign);

        // sign string
        Mac mac;
        try {
            mac = Mac.getInstance("HmacSHA1");
            mac.init(new SecretKeySpec(credential.getBytes(StandardCharsets.UTF_8), "HmacSHA1"));
        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        return BaseEncoding.base64().encode(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
    }

    private static byte[] signMessage(byte[] data, byte[] key, String algorithm)
            throws InvalidKeyException, NoSuchAlgorithmException {
        Mac mac = Mac.getInstance(algorithm);
        mac.init(new SecretKeySpec(key, algorithm));
        return mac.doFinal(data);
    }

    private static String getMessageDigest(byte[] payload, String algorithm) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance(algorithm);
        byte[] hash = md.digest(payload);
        return BaseEncoding.base16().lowerCase().encode(hash);
    }

    @Nullable
    private static String[] extractSignedHeaders(String authorization) {
        int index = authorization.indexOf("SignedHeaders=");
        if (index < 0) {
            return null;
        }
        int endSigned = authorization.indexOf(',', index);
        if (endSigned < 0) {
            return null;
        }
        int startHeaders = authorization.indexOf('=', index);
        return authorization.substring(startHeaders + 1, endSigned).split(";");
    }

    private static String buildCanonicalHeaders(HttpServletRequest request, String[] signedHeaders) {
        List<String> headers = new ArrayList<>();
        for (String header : signedHeaders) {
            headers.add(header.toLowerCase());
        }
        Collections.sort(headers);
        List<String> headersWithValues = new ArrayList<>();
        for (String header : headers) {
            List<String> values = new ArrayList<>();
            StringBuilder headerWithValue = new StringBuilder();
            headerWithValue.append(header);
            headerWithValue.append(":");
            for (String value : Collections.list(request.getHeaders(header))) {
                value = value.trim();
                if (!value.startsWith("\"")) {
                    value = value.replaceAll("\\s+", " ");
                }
                values.add(value);
            }
            headerWithValue.append(Joiner.on(",").join(values));
            headersWithValues.add(headerWithValue.toString());
        }

        return Joiner.on("\n").join(headersWithValues);
    }

    private static String buildCanonicalQueryString(HttpServletRequest request)
            throws UnsupportedEncodingException {
        // The parameters are required to be sorted
        List<String> parameters = Collections.list(request.getParameterNames());
        Collections.sort(parameters);
        List<String> queryParameters = new ArrayList<>();

        for (String key : parameters) {
            if (key.equals("X-Amz-Signature")) {
                continue;
            }
            // re-encode keys and values in AWS normalized form
            String value = request.getParameter(key);
            queryParameters
                    .add(AWS_URL_PARAMETER_ESCAPER.escape(key) + "=" + AWS_URL_PARAMETER_ESCAPER.escape(value));
        }
        return Joiner.on("&").join(queryParameters);
    }

    private static String createCanonicalRequest(HttpServletRequest request, String uri, byte[] payload,
            String hashAlgorithm) throws IOException, NoSuchAlgorithmException {
        String authorizationHeader = request.getHeader("Authorization");
        String xAmzContentSha256 = request.getHeader("x-amz-content-sha256");
        if (xAmzContentSha256 == null) {
            xAmzContentSha256 = request.getParameter("X-Amz-SignedHeaders");
        }
        String digest;
        if (authorizationHeader == null) {
            digest = "UNSIGNED-PAYLOAD";
        } else if ("STREAMING-AWS4-HMAC-SHA256-PAYLOAD".equals(xAmzContentSha256)) {
            digest = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
        } else if ("UNSIGNED-PAYLOAD".equals(xAmzContentSha256)) {
            digest = "UNSIGNED-PAYLOAD";
        } else {
            digest = getMessageDigest(payload, hashAlgorithm);
        }
        String[] signedHeaders;
        if (authorizationHeader != null) {
            signedHeaders = extractSignedHeaders(authorizationHeader);
        } else {
            signedHeaders = request.getParameter("X-Amz-SignedHeaders").split(";");
        }
        String canonicalRequest = Joiner.on("\n").join(request.getMethod(), uri, buildCanonicalQueryString(request),
                buildCanonicalHeaders(request, signedHeaders) + "\n", Joiner.on(';').join(signedHeaders), digest);
        return getMessageDigest(canonicalRequest.getBytes(StandardCharsets.UTF_8), hashAlgorithm);
    }

    /**
     * Create v4 signature.  Reference:
     * http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
     */
    static String createAuthorizationSignatureV4(HttpServletRequest request, S3AuthorizationHeader authHeader,
            byte[] payload, String uri, String credential)
            throws InvalidKeyException, IOException, NoSuchAlgorithmException, S3Exception {
        String canonicalRequest = createCanonicalRequest(request, uri, payload, authHeader.hashAlgorithm);
        String algorithm = authHeader.hmacAlgorithm;
        byte[] dateKey = signMessage(authHeader.date.getBytes(StandardCharsets.UTF_8),
                ("AWS4" + credential).getBytes(StandardCharsets.UTF_8), algorithm);
        byte[] dateRegionKey = signMessage(authHeader.region.getBytes(StandardCharsets.UTF_8), dateKey, algorithm);
        byte[] dateRegionServiceKey = signMessage(authHeader.service.getBytes(StandardCharsets.UTF_8),
                dateRegionKey, algorithm);
        byte[] signingKey = signMessage("aws4_request".getBytes(StandardCharsets.UTF_8), dateRegionServiceKey,
                algorithm);
        String date = request.getHeader("x-amz-date");
        if (date == null) {
            date = request.getParameter("X-Amz-Date");
        }
        String signatureString = "AWS4-HMAC-SHA256\n" + date + "\n" + authHeader.date + "/" + authHeader.region
                + "/s3/aws4_request\n" + canonicalRequest;
        byte[] signature = signMessage(signatureString.getBytes(StandardCharsets.UTF_8), signingKey, algorithm);
        return BaseEncoding.base16().lowerCase().encode(signature);
    }
}