001/* 002 * Copyright (c) 2011-2017 Nexmo Inc 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 020 * THE SOFTWARE. 021 */ 022package com.nexmo.client.auth; 023 024 025import java.io.UnsupportedEncodingException; 026import java.security.MessageDigest; 027import java.util.List; 028import java.util.Map; 029import java.util.TreeMap; 030 031import javax.servlet.http.HttpServletRequest; 032 033import com.nexmo.client.NexmoUnexpectedException; 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.apache.http.NameValuePair; 037import org.apache.http.message.BasicNameValuePair; 038 039/** 040 * A helper class for generating or verifying MD5 signatures when signing REST requests for submission to Nexmo. 041 * 042 * @author Paul Cook 043 */ 044public class RequestSigning { 045 public static final int MAX_ALLOWABLE_TIME_DELTA = 5 * 60 * 1000; 046 047 public static final String PARAM_SIGNATURE = "sig"; 048 public static final String PARAM_TIMESTAMP = "timestamp"; 049 050 private static Log log = LogFactory.getLog(RequestSigning.class); 051 052 /** 053 * Signs a set of request parameters. 054 * <p> 055 * Generates additional parameters to represent the timestamp and generated signature. 056 * Uses the supplied pre-shared secret key to generate the signature. 057 * 058 * @param params List of NameValuePair instances containing the query parameters for the request that is to be signed 059 * @param secretKey the pre-shared secret key held by the client 060 * 061 */ 062 public static void constructSignatureForRequestParameters(List<NameValuePair> params, String secretKey) { 063 constructSignatureForRequestParameters(params, secretKey, System.currentTimeMillis() / 1000); 064 } 065 066 /** 067 * Signs a set of request parameters. 068 * <p> 069 * Generates additional parameters to represent the timestamp and generated signature. 070 * Uses the supplied pre-shared secret key to generate the signature. 071 * 072 * @param params List of NameValuePair instances containing the query parameters for the request that is to be signed 073 * @param secretKey the pre-shared secret key held by the client 074 * @param currentTimeSeconds the current time in seconds since 1970-01-01 075 * 076 */ 077 protected static void constructSignatureForRequestParameters( 078 List<NameValuePair> params, String secretKey, long currentTimeSeconds) { 079 // First, inject a 'timestamp=' parameter containing the current time in seconds since Jan 1st 1970 080 params.add(new BasicNameValuePair(PARAM_TIMESTAMP, Long.toString(currentTimeSeconds))); 081 082 Map<String, String> sortedParams = new TreeMap<>(); 083 for (NameValuePair param: params) { 084 String name = param.getName(); 085 String value = param.getValue(); 086 if (name.equals(PARAM_SIGNATURE)) 087 continue; 088 if (value == null) 089 value = ""; 090 if (!value.trim().equals("")) 091 sortedParams.put(name, value); 092 } 093 094 // Now, walk through the sorted list of parameters and construct a string 095 StringBuilder sb = new StringBuilder(); 096 for (Map.Entry<String, String> param: sortedParams.entrySet()) { 097 String name = param.getKey(); 098 String value = param.getValue(); 099 sb.append("&").append(clean(name)).append("=").append(clean(value)); 100 } 101 102 // Now, append the secret key, and calculate an MD5 signature of the resultant string 103 sb.append(secretKey); 104 105 String str = sb.toString(); 106 107 String md5 = "no signature"; 108 try { 109 md5 = MD5Util.calculateMd5(str); 110 } catch (Exception e) { 111 log.error("error...", e); 112 } 113 114 log.debug("SECURITY-KEY-GENERATION -- String [ " + str + " ] Signature [ " + md5 + " ] "); 115 116 params.add(new BasicNameValuePair(PARAM_SIGNATURE, md5)); 117 } 118 119 /** 120 * Verifies the signature in an HttpServletRequest. 121 * 122 * @param request The HttpServletRequest to be verified 123 * @param secretKey The pre-shared secret key used by the sender of the request to create the signature 124 * 125 * @return true if the signature is correct for this request and secret key. 126 */ 127 public static boolean verifyRequestSignature(HttpServletRequest request, String secretKey) { 128 return verifyRequestSignature(request, secretKey, System.currentTimeMillis()); 129 } 130 131 /** 132 * Verifies the signature in an HttpServletRequest. 133 * 134 * @param request The HttpServletRequest to be verified 135 * @param secretKey The pre-shared secret key used by the sender of the request to create the signature 136 * @param currentTimeMillis The current time, in milliseconds. 137 * 138 * @return true if the signature is correct for this request and secret key. 139 */ 140 protected static boolean verifyRequestSignature(HttpServletRequest request, 141 String secretKey, 142 long currentTimeMillis) { 143 // identify the signature supplied in the request ... 144 String suppliedSignature = request.getParameter(PARAM_SIGNATURE); 145 if (suppliedSignature == null) 146 return false; 147 148 // Firstly, extract the timestamp parameter and verify that it is within 5 minutes of 'current time' 149 String timeString = request.getParameter(PARAM_TIMESTAMP); 150 long time = -1; 151 try { 152 if (timeString != null) 153 time = Long.parseLong(timeString) * 1000; 154 } catch (NumberFormatException e) { 155 log.error("Error parsing 'time' parameter [ " + timeString + " ]", e); 156 time = 0; 157 } 158 long diff = currentTimeMillis - time; 159 if (diff > MAX_ALLOWABLE_TIME_DELTA || diff < -MAX_ALLOWABLE_TIME_DELTA) { 160 log.warn("SECURITY-KEY-VERIFICATION -- BAD-TIMESTAMP ... Timestamp [ " + time + " ] delta [ " + diff + " ] max allowed delta [ " + -MAX_ALLOWABLE_TIME_DELTA + " ] "); 161 return false; 162 } 163 164 // Next, construct a sorted list of the name-value pair parameters supplied in the request, excluding the signature parameter 165 Map<String, String> sortedParams = new TreeMap<>(); 166 for (Map.Entry<String, String[]> entry: request.getParameterMap().entrySet()) { 167 String name = entry.getKey(); 168 String value = entry.getValue()[0]; 169 log.info("" + name + " = " + value); 170 if (name.equals(PARAM_SIGNATURE)) 171 continue; 172 if (value == null || value.trim().equals("")) { 173 continue; 174 } 175 sortedParams.put(name, value); 176 } 177 178 // walk this sorted list of parameters and construct a string 179 StringBuilder sb = new StringBuilder(); 180 for (Map.Entry<String, String> param: sortedParams.entrySet()) { 181 String name = param.getKey(); 182 String value = param.getValue(); 183 sb.append("&").append(clean(name)).append("=").append(clean(value)); 184 } 185 186 // append the secret key and calculate an md5 signature of the resultant string 187 sb.append(secretKey); 188 189 String str = sb.toString(); 190 191 String md5; 192 try { 193 md5 = MD5Util.calculateMd5(str); 194 } catch (Exception e) { 195 log.error("error...", e); 196 return false; 197 } 198 199 log.info("SECURITY-KEY-VERIFICATION -- String [ " + str + " ] Signature [ " + md5 + " ] SUPPLIED SIGNATURE [ " + suppliedSignature + " ] "); 200 201 // verify that the supplied signature matches generated one 202 // use MessageDigest.isEqual as an alternative to String.equals() to defend against timing based attacks 203 try { 204 if (!MessageDigest.isEqual(md5.getBytes("UTF-8"), suppliedSignature.getBytes("UTF-8"))) 205 return false; 206 } catch (UnsupportedEncodingException e) { 207 throw new NexmoUnexpectedException("Failed to decode signature as UTF-8", e); 208 } 209 210 return true; 211 } 212 213 public static String clean(String str) { 214 return str == null ? null : str.replaceAll("[=&]", "_"); 215 } 216 217}