001/*
002 *   Copyright 2020 Vonage
003 *
004 *   Licensed under the Apache License, Version 2.0 (the "License");
005 *   you may not use this file except in compliance with the License.
006 *   You may obtain a copy of the License at
007 *
008 *        http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *   Unless required by applicable law or agreed to in writing, software
011 *   distributed under the License is distributed on an "AS IS" BASIS,
012 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *   See the License for the specific language governing permissions and
014 *   limitations under the License.
015 */
016package com.vonage.client.auth;
017
018
019import com.vonage.client.VonageUnexpectedException;
020import org.apache.commons.logging.Log;
021import org.apache.commons.logging.LogFactory;
022import org.apache.http.NameValuePair;
023import org.apache.http.message.BasicNameValuePair;
024
025import javax.servlet.http.HttpServletRequest;
026import java.io.UnsupportedEncodingException;
027import java.security.MessageDigest;
028import java.util.List;
029import java.util.Map;
030import java.util.TreeMap;
031
032/**
033 * A helper class for generating or verifying MD5 signatures when signing REST requests for submission to Vonage.
034 */
035public class RequestSigning {
036    public static final int MAX_ALLOWABLE_TIME_DELTA = 5 * 60 * 1000;
037
038    public static final String PARAM_SIGNATURE = "sig";
039    public static final String PARAM_TIMESTAMP = "timestamp";
040
041    private static Log log = LogFactory.getLog(RequestSigning.class);
042
043    /**
044     * Signs a set of request parameters.
045     * <p>
046     * Generates additional parameters to represent the timestamp and generated signature.
047     * Uses the supplied pre-shared secret key to generate the signature.
048     *
049     * @param params List of NameValuePair instances containing the query parameters for the request that is to be signed
050     * @param secretKey the pre-shared secret key held by the client
051     *
052     */
053    public static void constructSignatureForRequestParameters(List<NameValuePair> params, String secretKey) {
054        constructSignatureForRequestParameters(params, secretKey, System.currentTimeMillis() / 1000);
055    }
056
057    /**
058     * Signs a set of request parameters.
059     * <p>
060     * Generates additional parameters to represent the timestamp and generated signature.
061     * Uses the supplied pre-shared secret key to generate the signature.
062     *
063     * @param params List of NameValuePair instances containing the query parameters for the request that is to be signed
064     * @param secretKey the pre-shared secret key held by the client
065     * @param currentTimeSeconds the current time in seconds since 1970-01-01
066     *
067     */
068     protected static void constructSignatureForRequestParameters(
069            List<NameValuePair> params, String secretKey, long currentTimeSeconds) {
070        // First, inject a 'timestamp=' parameter containing the current time in seconds since Jan 1st 1970
071        params.add(new BasicNameValuePair(PARAM_TIMESTAMP, Long.toString(currentTimeSeconds)));
072
073        Map<String, String> sortedParams = new TreeMap<>();
074        for (NameValuePair param: params) {
075            String name = param.getName();
076            String value = param.getValue();
077            if (name.equals(PARAM_SIGNATURE))
078                continue;
079            if (value == null)
080                value = "";
081            if (!value.trim().equals(""))
082                sortedParams.put(name, value);
083        }
084
085        // Now, walk through the sorted list of parameters and construct a string
086        StringBuilder sb = new StringBuilder();
087        for (Map.Entry<String, String> param: sortedParams.entrySet()) {
088            String name = param.getKey();
089            String value = param.getValue();
090            sb.append("&").append(clean(name)).append("=").append(clean(value));
091        }
092
093        // Now, append the secret key, and calculate an MD5 signature of the resultant string
094        sb.append(secretKey);
095
096        String str = sb.toString();
097
098        String md5 = "no signature";
099        try {
100            md5 = MD5Util.calculateMd5(str);
101        } catch (Exception e) {
102            log.error("error...", e);
103        }
104
105        log.debug("SECURITY-KEY-GENERATION -- String [ " + str + " ] Signature [ " + md5 + " ] ");
106
107        params.add(new BasicNameValuePair(PARAM_SIGNATURE, md5));
108    }
109
110    /**
111     * Verifies the signature in an HttpServletRequest.
112     *
113     * @param request The HttpServletRequest to be verified
114     * @param secretKey The pre-shared secret key used by the sender of the request to create the signature
115     *
116     * @return true if the signature is correct for this request and secret key.
117     */
118    public static boolean verifyRequestSignature(HttpServletRequest request, String secretKey) {
119        return verifyRequestSignature(request, secretKey, System.currentTimeMillis());
120    }
121
122    /**
123     * Verifies the signature in an HttpServletRequest.
124     *
125     * @param request The HttpServletRequest to be verified
126     * @param secretKey The pre-shared secret key used by the sender of the request to create the signature
127     * @param currentTimeMillis The current time, in milliseconds.
128     *
129     * @return true if the signature is correct for this request and secret key.
130     */
131     protected static boolean verifyRequestSignature(HttpServletRequest request,
132                                                     String secretKey,
133                                                     long currentTimeMillis) {
134        // identify the signature supplied in the request ...
135        String suppliedSignature = request.getParameter(PARAM_SIGNATURE);
136        if (suppliedSignature == null)
137            return false;
138
139        // Firstly, extract the timestamp parameter and verify that it is within 5 minutes of 'current time'
140        String timeString = request.getParameter(PARAM_TIMESTAMP);
141        long time = -1;
142        try {
143            if (timeString != null)
144                time = Long.parseLong(timeString) * 1000;
145        } catch (NumberFormatException e) {
146            log.error("Error parsing 'time' parameter [ " + timeString + " ]", e);
147            time = 0;
148        }
149        long diff = currentTimeMillis - time;
150        if (diff > MAX_ALLOWABLE_TIME_DELTA || diff < -MAX_ALLOWABLE_TIME_DELTA) {
151            log.warn("SECURITY-KEY-VERIFICATION -- BAD-TIMESTAMP ... Timestamp [ " + time + " ] delta [ " + diff + " ] max allowed delta [ " + -MAX_ALLOWABLE_TIME_DELTA + " ] ");
152            return false;
153        }
154
155        // Next, construct a sorted list of the name-value pair parameters supplied in the request, excluding the signature parameter
156        Map<String, String> sortedParams = new TreeMap<>();
157        for (Map.Entry<String, String[]> entry: request.getParameterMap().entrySet()) {
158            String name = entry.getKey();
159            String value = entry.getValue()[0];
160            log.info("" + name + " = " + value);
161            if (name.equals(PARAM_SIGNATURE))
162                continue;
163            if (value == null || value.trim().equals("")) {
164                continue;
165            }
166            sortedParams.put(name, value);
167        }
168
169        // walk this sorted list of parameters and construct a string
170        StringBuilder sb = new StringBuilder();
171        for (Map.Entry<String, String> param: sortedParams.entrySet()) {
172            String name = param.getKey();
173            String value = param.getValue();
174            sb.append("&").append(clean(name)).append("=").append(clean(value));
175        }
176
177        // append the secret key and calculate an md5 signature of the resultant string
178        sb.append(secretKey);
179
180        String str = sb.toString();
181
182        String md5;
183        try {
184            md5 = MD5Util.calculateMd5(str);
185        } catch (Exception e) {
186            log.error("error...", e);
187            return false;
188        }
189
190        log.info("SECURITY-KEY-VERIFICATION -- String [ " + str + " ] Signature [ " + md5 + " ] SUPPLIED SIGNATURE [ " + suppliedSignature + " ] ");
191
192        // verify that the supplied signature matches generated one
193        // use MessageDigest.isEqual as an alternative to String.equals() to defend against timing based attacks
194        try {
195            if (!MessageDigest.isEqual(md5.getBytes("UTF-8"), suppliedSignature.getBytes("UTF-8")))
196                return false;
197        } catch (UnsupportedEncodingException e) {
198            throw new VonageUnexpectedException("Failed to decode signature as UTF-8", e);
199        }
200
201        return true;
202    }
203
204    public static String clean(String str) {
205        return str == null ? null : str.replaceAll("[=&]", "_");
206    }
207
208}