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}