com.hp.autonomy.hod.client.util.Hmac.java Source code

Java tutorial

Introduction

Here is the source code for com.hp.autonomy.hod.client.util.Hmac.java

Source

/*
 * Copyright 2015-2016 Hewlett-Packard Development Company, L.P.
 * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
 */

package com.hp.autonomy.hod.client.util;

import com.hp.autonomy.hod.client.api.authentication.AuthenticationToken;
import com.hp.autonomy.hod.client.api.authentication.TokenType;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Hmac {
    private static final String HMAC_SHA1 = "HmacSHA1";
    private static final String UTF8 = "UTF-8";
    public static final String MD5 = "MD5";

    private static final String COLON = ":";
    private static final String EMPTY = "";
    private static final String NEW_LINE = "\n";

    /**
     * Generate the token header for an HMAC signed request to Haven OnDemand.
     * @param request The request to authenticate
     * @param token The HMAC SHA1 authentication token
     * @return The token parameter for the request
     */
    public String generateToken(final Request<?, ?> request,
            final AuthenticationToken<?, TokenType.HmacSha1> token) {
        return generateTokenHelper(request, token);
    }

    // So we don't have to expose type parameters on the generateToken interface
    private <Q, B> String generateTokenHelper(final Request<Q, B> request,
            final AuthenticationToken<?, TokenType.HmacSha1> token) {
        final String bodyHash = createBodyHash(request.getBody());
        final String message = createMessage(request, bodyHash);
        final String signature = base64EncodeForUri(hmacSha1(message, token.getSecret()));

        final List<String> components = Arrays.asList(token.getType(), token.getId(), bodyHash, signature);

        return StringUtils.join(components, COLON);
    }

    // Creates the representation of the request for HMAC signing, given a request and it's body hash
    private <Q, B> String createMessage(final Request<Q, B> request, final String bodyHash) {
        final List<String> components = new LinkedList<>();
        components.add(encodeVerb(request.getVerb()));
        components.add(encodePath(request.getPath()));
        components.addAll(encodeQueryParameters(request.getQueryParameters()));
        components.add(urlEncode(bodyHash));
        return StringUtils.join(components, NEW_LINE);
    }

    private <T> List<String> encodeQueryParameters(final Map<String, List<T>> queryParameters) {
        if (queryParameters == null || queryParameters.isEmpty()) {
            return Collections.emptyList();
        } else {
            return encodeAndSpreadParameterMap(queryParameters, input -> urlEncode(input.toString()));
        }
    }

    private <T> String createBodyHash(final Map<String, List<T>> body) {
        if (body == null || body.isEmpty()) {
            // If no body, the body hash must be the empty string
            return EMPTY;
        } else {
            final List<String> components = encodeAndSpreadParameterMap(body, input -> {
                final byte[] bytes;

                if (input instanceof byte[]) {
                    bytes = (byte[]) input;
                } else {
                    bytes = bytesFromString(input.toString());
                }

                return Hex.encodeHexString(md5Hash(bytes));
            });

            final String bodyRepresentation = StringUtils.join(components, NEW_LINE);
            return base64EncodeForUri(md5Hash(bytesFromString(bodyRepresentation)));
        }
    }

    /*
    Takes a map of parameter name to list of parameter values. URI encodes every key and encodes every value using
    the ValueEncoder, then returns a list containing each encoded value adjacent to it's encoded key. The list is
    sorted by the encoded key, but maintains the order of each value associated with a given key.
        
    {key1: [value11, value12], key2: [value21]}
    uri(key2) < uri(key1)
        
    => [uri(key2), encode(value21), uri(key1), encode(value11), uri(key1), encode(value12)]
    */
    private <T> List<String> encodeAndSpreadParameterMap(final Map<String, List<T>> parameterMap,
            final ValueEncoder encoder) {
        final List<Parameter> parameters = new LinkedList<>();

        for (final Map.Entry<String, List<T>> entry : parameterMap.entrySet()) {
            final String encodedKey = urlEncode(entry.getKey());
            final List<?> values = entry.getValue();

            parameters.addAll(values.stream().map(value -> new Parameter(encodedKey, encoder.encode(value)))
                    .collect(Collectors.toList()));
        }

        // Sort is guaranteed to be stable, so parameters for a given key will remain in the same order
        Collections.sort(parameters);

        final List<String> components = new LinkedList<>();

        for (final Parameter parameter : parameters) {
            components.add(parameter.key);
            components.add(parameter.value);
        }

        return components;
    }

    private String encodePath(final String path) {
        // Path must be url encoded and have no leading or trailing slashes
        return urlEncode(path.replaceAll("^/|/$", EMPTY));
    }

    private String encodeVerb(final Request.Verb verb) {
        return verb.name();
    }

    private byte[] bytesFromString(final String input) {
        try {
            return input.getBytes(UTF8);
        } catch (final UnsupportedEncodingException e) {
            // This should never happen on a sensible JVM
            throw new AssertionError("UTF8 is not supported", e);
        }
    }

    private String base64EncodeForUri(final byte[] bytes) {
        // Some base64 characters are not valid in a URI
        return Base64.encodeBase64String(bytes).replaceAll("=", EMPTY).replaceAll("/", "-").replaceAll("[+]", "_");
    }

    private String urlEncode(final String input) {
        try {
            final String encode = URLEncoder.encode(input, UTF8);

            // Haven OnDemand expects space to be encoded as %20, not plus
            return encode.replaceAll("\\+", "%20");
        } catch (final UnsupportedEncodingException e) {
            // This should never happen on a sensible JVM
            throw new AssertionError("UTF8 is not supported", e);
        }
    }

    private byte[] md5Hash(final byte[] input) {
        try {
            return MessageDigest.getInstance(MD5).digest(input);
        } catch (final NoSuchAlgorithmException e) {
            // This should never happen on a sensible JVM
            throw new AssertionError("UTF8 or MD5 is not supported");
        }
    }

    private byte[] hmacSha1(final String message, final String secret) {
        try {
            final Mac mac = Mac.getInstance(HMAC_SHA1);
            final Key key = new SecretKeySpec(bytesFromString(secret), HMAC_SHA1);
            mac.init(key);
            return mac.doFinal(bytesFromString(message));
        } catch (final NoSuchAlgorithmException e) {
            // This should never happen on a sensible JVM
            throw new AssertionError("HMAC SHA1 is not supported", e);
        } catch (final InvalidKeyException e) {
            // In practice, this means that the token secret was invalid
            throw new IllegalArgumentException("Invalid token secret", e);
        }
    }

    private static class Parameter implements Comparable<Parameter> {
        private final String key;
        private final String value;

        private Parameter(final String key, final String value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public int compareTo(final Parameter other) {
            return key.compareTo(other.key);
        }
    }

    private interface ValueEncoder {
        String encode(Object input);
    }
}