package org.azavea.otm.rest; import android.text.TextUtils; import android.util.Base64; import com.loopj.android.http.RequestParams; import org.azavea.helpers.Logger; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.security.SignatureException; import java.util.Arrays; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import cz.msebera.android.httpclient.Header; import cz.msebera.android.httpclient.message.BasicHeader; public class RequestSignature { private static final String HMAC_ALGORITHM = "HmacSHA256"; private final String secretKey; public RequestSignature(String secretKey) { this.secretKey = secretKey; } /** * All api calls are required to be signed using HMAC based on the request * string: {Http Verb}\n{host}\n{path}\n{k=v...}{body} where the query * parameters are byte ordered * <p> * This assumes that the URL has been constructed with the access key and * the timestamp already appended. */ public String getSignature(String verb, String url, RequestParams params, String body) throws UnsupportedEncodingException, URISyntaxException, SignatureException { // Add the params to the existing query string arguments, or add as new String separator = url.contains("?") ? "&" : "?"; url += separator + params.toString(); return getSignature(verb, url, body.getBytes("UTF-8")); } /** * All api calls are required to be signed using HMAC based on the request * string: {Http Verb}\n{host}\n{path}\n{k=v...}{body} where the query * parameters are byte ordered * <p> * This assumes that the URL has been constructed with the access key and * the timestamp already appended. */ public String getSignature(String verb, String url, byte[] body) throws URISyntaxException, SignatureException { URI uri = new URI(url); String hostWithPort = uri.getAuthority(); String path = uri.getPath(); // Signature is generated against query arguments sorted by key String[] query = uri.getQuery() != null ? uri.getQuery().split("&") : new String[]{}; Arrays.sort(query); // The value of each query param must be URLEncoded, which isn't // reliable from URI.getQuery or .getRawQuery. Some values are // encoded on the fly during actual request, so do it manually here for (int i = 0; i < query.length; i++) { String[] kv = query[i].split("="); String encodedVal = URLEncoder.encode(kv[1]); query[i] = kv[0] + "=" + encodedVal; } String sortedQuery = TextUtils.join("&", query); String payload = verb + "\n" + hostWithPort + "\n" + path + "\n" + sortedQuery + Base64.encodeToString(body, Base64.NO_WRAP); String signature = calculateHMAC(payload); return signature; } /** * Generate HMAC API signature header to include in all requests */ public Header getSignatureHeader(String verb, String url, RequestParams params) throws UnsupportedEncodingException, URISyntaxException, SignatureException { return getSignatureHeader(verb, url, params, ""); } public Header getSignatureHeader(String verb, String url, String body) throws UnsupportedEncodingException, URISyntaxException, SignatureException { return getSignatureHeader(verb, url, body.getBytes("UTF-8")); } public Header getSignatureHeader(String verb, String url, byte[] body) throws URISyntaxException, SignatureException { String sig = getSignature(verb, url, body); return new BasicHeader("X-Signature", sig); } public Header getSignatureHeader(String verb, String url, RequestParams params, String body) throws UnsupportedEncodingException, URISyntaxException, SignatureException { String sig = getSignature(verb, url, params, body); return new BasicHeader("X-Signature", sig); } /** * Computes RFC 2104-compliant HMAC signature. * * @param data The data to be signed. * @return The Base64-encoded RFC 2104-compliant HMAC signature. * @throws SignatureException */ public String calculateHMAC(String data) throws SignatureException { String result; try { // Get an hmac key from the raw key bytes SecretKeySpec signingKey = new SecretKeySpec(this.secretKey.getBytes(), HMAC_ALGORITHM); Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(signingKey); // Compute the hmac on input data bytes byte[] rawHmac = mac.doFinal(data.getBytes()); result = Base64.encodeToString(rawHmac, Base64.NO_WRAP); } catch (Exception ex) { Logger.error("Failed to generate HMAC for API", ex); throw new SignatureException("Could not sign API request"); } return result; } }