001package com.pusher.rest; 002 003import java.io.UnsupportedEncodingException; 004import java.net.URI; 005import java.net.URISyntaxException; 006import java.security.InvalidKeyException; 007import java.security.MessageDigest; 008import java.security.NoSuchAlgorithmException; 009import java.util.Arrays; 010import java.util.HashMap; 011import java.util.Map; 012import java.util.Map.Entry; 013 014import javax.crypto.Mac; 015import javax.crypto.spec.SecretKeySpec; 016 017import org.apache.commons.codec.binary.Hex; 018import org.apache.http.client.utils.URIBuilder; 019 020import com.pusher.rest.util.Prerequisites; 021 022public class SignatureUtil { 023 024 public static URI uri(final String method, 025 final String scheme, 026 final String host, 027 final String path, 028 final String body, 029 final String key, 030 final String secret, 031 final Map<String, String> extraParams) { 032 033 Prerequisites.noReservedKeys(extraParams); 034 035 try { 036 final Map<String, String> allParams = new HashMap<String, String>(extraParams); 037 allParams.put("auth_key", key); 038 allParams.put("auth_version", "1.0"); 039 allParams.put("auth_timestamp", new Long(System.currentTimeMillis() / 1000).toString()); 040 if (body != null) { 041 allParams.put("body_md5", bodyMd5(body)); 042 } 043 044 // This is where the auth gets a bit weird. The query params for the request must include 045 // the auth signature which is a signature over all the params except itself. 046 allParams.put("auth_signature", sign(buildSignatureString(method, path, allParams), secret)); 047 048 final URIBuilder b = new URIBuilder() 049 .setScheme(scheme) 050 .setHost(host) 051 .setPath(path); 052 053 for (final Entry<String, String> e : allParams.entrySet()) { 054 b.setParameter(e.getKey(), e.getValue()); 055 } 056 057 return b.build(); 058 } 059 catch (final URISyntaxException e) { 060 throw new RuntimeException("Could not build URI", e); 061 } 062 } 063 064 private static String bodyMd5(final String body) { 065 try { 066 final MessageDigest md = MessageDigest.getInstance("MD5"); 067 final byte[] digest = md.digest(body.getBytes("UTF-8")); 068 return Hex.encodeHexString(digest); 069 } 070 // If this doesn't exist, we're pretty much out of luck. 071 catch (final NoSuchAlgorithmException e) { 072 throw new RuntimeException("The Pusher HTTP client requires MD5 support", e); 073 } 074 catch (final UnsupportedEncodingException e) { 075 throw new RuntimeException("The Pusher HTTP client needs UTF-8 support", e); 076 } 077 } 078 079 public static String sign(final String input, final String secret) { 080 try { 081 final Mac mac = Mac.getInstance("HmacSHA256"); 082 mac.init(new SecretKeySpec(secret.getBytes(), "SHA256")); 083 084 final byte[] digest = mac.doFinal(input.getBytes("UTF-8")); 085 return Hex.encodeHexString(digest); 086 } 087 catch (final InvalidKeyException e) { 088 /// We validate this when the key is first provided, so we should never encounter it here. 089 throw new RuntimeException("Invalid secret key", e); 090 } 091 // If either of these doesn't exist, we're pretty much out of luck. 092 catch (final NoSuchAlgorithmException e) { 093 throw new RuntimeException("The Pusher HTTP client requires HmacSHA256 support", e); 094 } 095 catch (final UnsupportedEncodingException e) { 096 throw new RuntimeException("The Pusher HTTP client needs UTF-8 support", e); 097 } 098 } 099 100 // Visible for testing 101 static String buildSignatureString(final String method, final String path, final Map<String, String> queryParams) { 102 final StringBuilder sb = new StringBuilder(); 103 sb.append(method) 104 .append('\n') 105 .append(path) 106 .append('\n'); 107 108 final String[] keys = queryParams.keySet().toArray(new String[0]); 109 Arrays.sort(keys); 110 111 boolean first = true; 112 for (final String key : keys) { 113 if (!first) sb.append('&'); 114 else first = false; 115 116 sb.append(key) 117 .append('=') 118 .append(queryParams.get(key)); 119 } 120 121 return sb.toString(); 122 } 123}