1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/net/HawkAuthHeaderProvider.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,403 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync.net; 1.9 + 1.10 +import java.io.IOException; 1.11 +import java.io.InputStream; 1.12 +import java.io.UnsupportedEncodingException; 1.13 +import java.net.URI; 1.14 +import java.security.GeneralSecurityException; 1.15 +import java.security.InvalidKeyException; 1.16 +import java.security.MessageDigest; 1.17 +import java.security.NoSuchAlgorithmException; 1.18 +import java.util.Locale; 1.19 + 1.20 +import javax.crypto.Mac; 1.21 +import javax.crypto.spec.SecretKeySpec; 1.22 + 1.23 +import org.mozilla.apache.commons.codec.binary.Base64; 1.24 +import org.mozilla.gecko.background.common.log.Logger; 1.25 +import org.mozilla.gecko.sync.Utils; 1.26 + 1.27 +import ch.boye.httpclientandroidlib.Header; 1.28 +import ch.boye.httpclientandroidlib.HttpEntity; 1.29 +import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest; 1.30 +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; 1.31 +import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; 1.32 +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; 1.33 +import ch.boye.httpclientandroidlib.message.BasicHeader; 1.34 +import ch.boye.httpclientandroidlib.protocol.BasicHttpContext; 1.35 + 1.36 +/** 1.37 + * An <code>AuthHeaderProvider</code> that returns an Authorization header for 1.38 + * Hawk: <a href="https://github.com/hueniverse/hawk">https://github.com/hueniverse/hawk</a>. 1.39 + * 1.40 + * Hawk is an HTTP authentication scheme using a message authentication code 1.41 + * (MAC) algorithm to provide partial HTTP request cryptographic verification. 1.42 + * Hawk is the successor to the HMAC authentication scheme. 1.43 + */ 1.44 +public class HawkAuthHeaderProvider implements AuthHeaderProvider { 1.45 + public static final String LOG_TAG = HawkAuthHeaderProvider.class.getSimpleName(); 1.46 + 1.47 + public static final int HAWK_HEADER_VERSION = 1; 1.48 + 1.49 + protected static final int NONCE_LENGTH_IN_BYTES = 8; 1.50 + protected static final String HMAC_SHA256_ALGORITHM = "hmacSHA256"; 1.51 + 1.52 + protected final String id; 1.53 + protected final byte[] key; 1.54 + protected final boolean includePayloadHash; 1.55 + protected final long skewSeconds; 1.56 + 1.57 + /** 1.58 + * Create a Hawk Authorization header provider. 1.59 + * <p> 1.60 + * Hawk specifies no mechanism by which a client receives an 1.61 + * identifier-and-key pair from the server. 1.62 + * <p> 1.63 + * Hawk requests can include a payload verification hash with requests that 1.64 + * enclose an entity (PATCH, POST, and PUT requests). <b>You should default 1.65 + * to including the payload verification hash<b> unless you have a good reason 1.66 + * not to -- the server can always ignore payload verification hashes provided 1.67 + * by the client. 1.68 + * 1.69 + * @param id 1.70 + * to name requests with. 1.71 + * @param key 1.72 + * to sign request with. 1.73 + * 1.74 + * @param includePayloadHash 1.75 + * true if payload verification hash should be included in signed 1.76 + * request header. See <a href="https://github.com/hueniverse/hawk#payload-validation">https://github.com/hueniverse/hawk#payload-validation</a>. 1.77 + * 1.78 + * @param skewSeconds 1.79 + * a number of seconds by which to skew the current time when 1.80 + * computing a header. 1.81 + */ 1.82 + public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash, long skewSeconds) { 1.83 + if (id == null) { 1.84 + throw new IllegalArgumentException("id must not be null"); 1.85 + } 1.86 + if (key == null) { 1.87 + throw new IllegalArgumentException("key must not be null"); 1.88 + } 1.89 + this.id = id; 1.90 + this.key = key; 1.91 + this.includePayloadHash = includePayloadHash; 1.92 + this.skewSeconds = skewSeconds; 1.93 + } 1.94 + 1.95 + /** 1.96 + * @return the current time in milliseconds. 1.97 + */ 1.98 + @SuppressWarnings("static-method") 1.99 + protected long now() { 1.100 + return System.currentTimeMillis(); 1.101 + } 1.102 + 1.103 + /** 1.104 + * @return the current time in seconds, adjusted for skew. This should 1.105 + * approximate the server's timestamp. 1.106 + */ 1.107 + protected long getTimestampSeconds() { 1.108 + return (now() / 1000) + skewSeconds; 1.109 + } 1.110 + 1.111 + @Override 1.112 + public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException { 1.113 + long timestamp = getTimestampSeconds(); 1.114 + String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES)); 1.115 + String extra = ""; 1.116 + 1.117 + try { 1.118 + return getAuthHeader(request, context, client, timestamp, nonce, extra, this.includePayloadHash); 1.119 + } catch (Exception e) { 1.120 + // We lie a little and make every exception a GeneralSecurityException. 1.121 + throw new GeneralSecurityException(e); 1.122 + } 1.123 + } 1.124 + 1.125 + /** 1.126 + * Helper function that generates an HTTP Authorization: Hawk header given 1.127 + * additional Hawk specific data. 1.128 + * 1.129 + * @throws NoSuchAlgorithmException 1.130 + * @throws InvalidKeyException 1.131 + * @throws IOException 1.132 + */ 1.133 + protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client, 1.134 + long timestamp, String nonce, String extra, boolean includePayloadHash) 1.135 + throws InvalidKeyException, NoSuchAlgorithmException, IOException { 1.136 + if (timestamp < 0) { 1.137 + throw new IllegalArgumentException("timestamp must contain only [0-9]."); 1.138 + } 1.139 + 1.140 + if (nonce == null) { 1.141 + throw new IllegalArgumentException("nonce must not be null."); 1.142 + } 1.143 + if (nonce.length() == 0) { 1.144 + throw new IllegalArgumentException("nonce must not be empty."); 1.145 + } 1.146 + 1.147 + String payloadHash = null; 1.148 + if (includePayloadHash) { 1.149 + payloadHash = getPayloadHashString(request); 1.150 + } else { 1.151 + Logger.debug(LOG_TAG, "Configured to not include payload hash for this request."); 1.152 + } 1.153 + 1.154 + String app = null; 1.155 + String dlg = null; 1.156 + String requestString = getRequestString(request, "header", timestamp, nonce, payloadHash, extra, app, dlg); 1.157 + String macString = getSignature(requestString.getBytes("UTF-8"), this.key); 1.158 + 1.159 + StringBuilder sb = new StringBuilder(); 1.160 + sb.append("Hawk id=\""); 1.161 + sb.append(this.id); 1.162 + sb.append("\", "); 1.163 + sb.append("ts=\""); 1.164 + sb.append(timestamp); 1.165 + sb.append("\", "); 1.166 + sb.append("nonce=\""); 1.167 + sb.append(nonce); 1.168 + sb.append("\", "); 1.169 + if (payloadHash != null) { 1.170 + sb.append("hash=\""); 1.171 + sb.append(payloadHash); 1.172 + sb.append("\", "); 1.173 + } 1.174 + if (extra != null && extra.length() > 0) { 1.175 + sb.append("ext=\""); 1.176 + sb.append(escapeExtraHeaderAttribute(extra)); 1.177 + sb.append("\", "); 1.178 + } 1.179 + sb.append("mac=\""); 1.180 + sb.append(macString); 1.181 + sb.append("\""); 1.182 + 1.183 + return new BasicHeader("Authorization", sb.toString()); 1.184 + } 1.185 + 1.186 + /** 1.187 + * Get the payload verification hash for the given request, if possible. 1.188 + * <p> 1.189 + * Returns null if the request does not enclose an entity (is not an HTTP 1.190 + * PATCH, POST, or PUT). Throws if the payload verification hash cannot be 1.191 + * computed. 1.192 + * 1.193 + * @param request 1.194 + * to compute hash for. 1.195 + * @return verification hash, or null if the request does not enclose an entity. 1.196 + * @throws IllegalArgumentException if the request does not enclose a valid non-null entity. 1.197 + * @throws UnsupportedEncodingException 1.198 + * @throws NoSuchAlgorithmException 1.199 + * @throws IOException 1.200 + */ 1.201 + protected static String getPayloadHashString(HttpRequestBase request) 1.202 + throws UnsupportedEncodingException, NoSuchAlgorithmException, IOException, IllegalArgumentException { 1.203 + final boolean shouldComputePayloadHash = request instanceof HttpEntityEnclosingRequest; 1.204 + if (!shouldComputePayloadHash) { 1.205 + Logger.debug(LOG_TAG, "Not computing payload verification hash for non-enclosing request."); 1.206 + return null; 1.207 + } 1.208 + if (!(request instanceof HttpEntityEnclosingRequest)) { 1.209 + throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request without an entity"); 1.210 + } 1.211 + final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); 1.212 + if (entity == null) { 1.213 + throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request with a null entity"); 1.214 + } 1.215 + return Base64.encodeBase64String(getPayloadHash(entity)); 1.216 + } 1.217 + 1.218 + /** 1.219 + * Escape the user-provided extra string for the ext="" header attribute. 1.220 + * <p> 1.221 + * Hawk escapes the header ext="" attribute differently than it does the extra 1.222 + * line in the normalized request string. 1.223 + * <p> 1.224 + * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385</a>. 1.225 + * 1.226 + * @param extra to escape. 1.227 + * @return extra escaped for the ext="" header attribute. 1.228 + */ 1.229 + protected static String escapeExtraHeaderAttribute(String extra) { 1.230 + return extra.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\""); 1.231 + } 1.232 + 1.233 + /** 1.234 + * Escape the user-provided extra string for inserting into the normalized 1.235 + * request string. 1.236 + * <p> 1.237 + * Hawk escapes the header ext="" attribute differently than it does the extra 1.238 + * line in the normalized request string. 1.239 + * <p> 1.240 + * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67</a>. 1.241 + * 1.242 + * @param extra to escape. 1.243 + * @return extra escaped for the normalized request string. 1.244 + */ 1.245 + protected static String escapeExtraString(String extra) { 1.246 + return extra.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\n"); 1.247 + } 1.248 + 1.249 + /** 1.250 + * Return the content type with no parameters (pieces following ;). 1.251 + * 1.252 + * @param contentTypeHeader to interrogate. 1.253 + * @return base content type. 1.254 + */ 1.255 + protected static String getBaseContentType(Header contentTypeHeader) { 1.256 + if (contentTypeHeader == null) { 1.257 + throw new IllegalArgumentException("contentTypeHeader must not be null."); 1.258 + } 1.259 + String contentType = contentTypeHeader.getValue(); 1.260 + if (contentType == null) { 1.261 + throw new IllegalArgumentException("contentTypeHeader value must not be null."); 1.262 + } 1.263 + int index = contentType.indexOf(";"); 1.264 + if (index < 0) { 1.265 + return contentType.trim(); 1.266 + } 1.267 + return contentType.substring(0, index).trim(); 1.268 + } 1.269 + 1.270 + /** 1.271 + * Generate the SHA-256 hash of a normalized Hawk payload generated from an 1.272 + * HTTP entity. 1.273 + * <p> 1.274 + * <b>Warning:</b> the entity <b>must</b> be repeatable. If it is not, this 1.275 + * code throws an <code>IllegalArgumentException</code>. 1.276 + * <p> 1.277 + * This is under-specified; the code here was reverse engineered from the code 1.278 + * at 1.279 + * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81</a>. 1.280 + * @param entity to normalize and hash. 1.281 + * @return hash. 1.282 + * @throws IllegalArgumentException if entity is not repeatable. 1.283 + */ 1.284 + protected static byte[] getPayloadHash(HttpEntity entity) throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException { 1.285 + if (!entity.isRepeatable()) { 1.286 + throw new IllegalArgumentException("entity must be repeatable"); 1.287 + } 1.288 + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); 1.289 + digest.update(("hawk." + HAWK_HEADER_VERSION + ".payload\n").getBytes("UTF-8")); 1.290 + digest.update(getBaseContentType(entity.getContentType()).getBytes("UTF-8")); 1.291 + digest.update("\n".getBytes("UTF-8")); 1.292 + InputStream stream = entity.getContent(); 1.293 + try { 1.294 + int numRead; 1.295 + byte[] buffer = new byte[4096]; 1.296 + while (-1 != (numRead = stream.read(buffer))) { 1.297 + if (numRead > 0) { 1.298 + digest.update(buffer, 0, numRead); 1.299 + } 1.300 + } 1.301 + digest.update("\n".getBytes("UTF-8")); // Trailing newline is specified by Hawk. 1.302 + return digest.digest(); 1.303 + } finally { 1.304 + stream.close(); 1.305 + } 1.306 + } 1.307 + 1.308 + /** 1.309 + * Generate a normalized Hawk request string. This is under-specified; the 1.310 + * code here was reverse engineered from the code at 1.311 + * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55</a>. 1.312 + * <p> 1.313 + * This method trusts its inputs to be valid. 1.314 + */ 1.315 + protected static String getRequestString(HttpUriRequest request, String type, long timestamp, String nonce, String hash, String extra, String app, String dlg) { 1.316 + String method = request.getMethod().toUpperCase(Locale.US); 1.317 + 1.318 + URI uri = request.getURI(); 1.319 + String host = uri.getHost(); 1.320 + 1.321 + String path = uri.getRawPath(); 1.322 + if (uri.getRawQuery() != null) { 1.323 + path += "?"; 1.324 + path += uri.getRawQuery(); 1.325 + } 1.326 + if (uri.getRawFragment() != null) { 1.327 + path += "#"; 1.328 + path += uri.getRawFragment(); 1.329 + } 1.330 + 1.331 + int port = uri.getPort(); 1.332 + String scheme = uri.getScheme(); 1.333 + if (port != -1) { 1.334 + } else if ("http".equalsIgnoreCase(scheme)) { 1.335 + port = 80; 1.336 + } else if ("https".equalsIgnoreCase(scheme)) { 1.337 + port = 443; 1.338 + } else { 1.339 + throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + "."); 1.340 + } 1.341 + 1.342 + StringBuilder sb = new StringBuilder(); 1.343 + sb.append("hawk."); 1.344 + sb.append(HAWK_HEADER_VERSION); 1.345 + sb.append('.'); 1.346 + sb.append(type); 1.347 + sb.append('\n'); 1.348 + sb.append(timestamp); 1.349 + sb.append('\n'); 1.350 + sb.append(nonce); 1.351 + sb.append('\n'); 1.352 + sb.append(method); 1.353 + sb.append('\n'); 1.354 + sb.append(path); 1.355 + sb.append('\n'); 1.356 + sb.append(host); 1.357 + sb.append('\n'); 1.358 + sb.append(port); 1.359 + sb.append('\n'); 1.360 + if (hash != null) { 1.361 + sb.append(hash); 1.362 + } 1.363 + sb.append("\n"); 1.364 + if (extra != null && extra.length() > 0) { 1.365 + sb.append(escapeExtraString(extra)); 1.366 + } 1.367 + sb.append("\n"); 1.368 + if (app != null) { 1.369 + sb.append(app); 1.370 + sb.append("\n"); 1.371 + if (dlg != null) { 1.372 + sb.append(dlg); 1.373 + } 1.374 + sb.append("\n"); 1.375 + } 1.376 + 1.377 + return sb.toString(); 1.378 + } 1.379 + 1.380 + protected static byte[] hmacSha256(byte[] message, byte[] key) 1.381 + throws NoSuchAlgorithmException, InvalidKeyException { 1.382 + 1.383 + SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA256_ALGORITHM); 1.384 + 1.385 + Mac hasher = Mac.getInstance(HMAC_SHA256_ALGORITHM); 1.386 + hasher.init(keySpec); 1.387 + hasher.update(message); 1.388 + 1.389 + return hasher.doFinal(); 1.390 + } 1.391 + 1.392 + /** 1.393 + * Sign a Hawk request string. 1.394 + * 1.395 + * @param requestString to sign. 1.396 + * @param key as <code>String</code>. 1.397 + * @return signature as base-64 encoded string. 1.398 + * @throws InvalidKeyException 1.399 + * @throws NoSuchAlgorithmException 1.400 + * @throws UnsupportedEncodingException 1.401 + */ 1.402 + protected static String getSignature(byte[] requestString, byte[] key) 1.403 + throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException { 1.404 + return Base64.encodeBase64String(hmacSha256(requestString, key)); 1.405 + } 1.406 +}