diff -r 000000000000 -r 6474c204b198 mobile/android/base/sync/net/HawkAuthHeaderProvider.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mobile/android/base/sync/net/HawkAuthHeaderProvider.java Wed Dec 31 06:09:35 2014 +0100
@@ -0,0 +1,403 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.Utils;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
+
+/**
+ * An AuthHeaderProvider
that returns an Authorization header for
+ * Hawk: https://github.com/hueniverse/hawk.
+ *
+ * Hawk is an HTTP authentication scheme using a message authentication code
+ * (MAC) algorithm to provide partial HTTP request cryptographic verification.
+ * Hawk is the successor to the HMAC authentication scheme.
+ */
+public class HawkAuthHeaderProvider implements AuthHeaderProvider {
+ public static final String LOG_TAG = HawkAuthHeaderProvider.class.getSimpleName();
+
+ public static final int HAWK_HEADER_VERSION = 1;
+
+ protected static final int NONCE_LENGTH_IN_BYTES = 8;
+ protected static final String HMAC_SHA256_ALGORITHM = "hmacSHA256";
+
+ protected final String id;
+ protected final byte[] key;
+ protected final boolean includePayloadHash;
+ protected final long skewSeconds;
+
+ /**
+ * Create a Hawk Authorization header provider.
+ *
+ * Hawk specifies no mechanism by which a client receives an + * identifier-and-key pair from the server. + *
+ * Hawk requests can include a payload verification hash with requests that + * enclose an entity (PATCH, POST, and PUT requests). You should default + * to including the payload verification hash unless you have a good reason + * not to -- the server can always ignore payload verification hashes provided + * by the client. + * + * @param id + * to name requests with. + * @param key + * to sign request with. + * + * @param includePayloadHash + * true if payload verification hash should be included in signed + * request header. See https://github.com/hueniverse/hawk#payload-validation. + * + * @param skewSeconds + * a number of seconds by which to skew the current time when + * computing a header. + */ + public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash, long skewSeconds) { + if (id == null) { + throw new IllegalArgumentException("id must not be null"); + } + if (key == null) { + throw new IllegalArgumentException("key must not be null"); + } + this.id = id; + this.key = key; + this.includePayloadHash = includePayloadHash; + this.skewSeconds = skewSeconds; + } + + /** + * @return the current time in milliseconds. + */ + @SuppressWarnings("static-method") + protected long now() { + return System.currentTimeMillis(); + } + + /** + * @return the current time in seconds, adjusted for skew. This should + * approximate the server's timestamp. + */ + protected long getTimestampSeconds() { + return (now() / 1000) + skewSeconds; + } + + @Override + public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException { + long timestamp = getTimestampSeconds(); + String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES)); + String extra = ""; + + try { + return getAuthHeader(request, context, client, timestamp, nonce, extra, this.includePayloadHash); + } catch (Exception e) { + // We lie a little and make every exception a GeneralSecurityException. + throw new GeneralSecurityException(e); + } + } + + /** + * Helper function that generates an HTTP Authorization: Hawk header given + * additional Hawk specific data. + * + * @throws NoSuchAlgorithmException + * @throws InvalidKeyException + * @throws IOException + */ + protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client, + long timestamp, String nonce, String extra, boolean includePayloadHash) + throws InvalidKeyException, NoSuchAlgorithmException, IOException { + if (timestamp < 0) { + throw new IllegalArgumentException("timestamp must contain only [0-9]."); + } + + if (nonce == null) { + throw new IllegalArgumentException("nonce must not be null."); + } + if (nonce.length() == 0) { + throw new IllegalArgumentException("nonce must not be empty."); + } + + String payloadHash = null; + if (includePayloadHash) { + payloadHash = getPayloadHashString(request); + } else { + Logger.debug(LOG_TAG, "Configured to not include payload hash for this request."); + } + + String app = null; + String dlg = null; + String requestString = getRequestString(request, "header", timestamp, nonce, payloadHash, extra, app, dlg); + String macString = getSignature(requestString.getBytes("UTF-8"), this.key); + + StringBuilder sb = new StringBuilder(); + sb.append("Hawk id=\""); + sb.append(this.id); + sb.append("\", "); + sb.append("ts=\""); + sb.append(timestamp); + sb.append("\", "); + sb.append("nonce=\""); + sb.append(nonce); + sb.append("\", "); + if (payloadHash != null) { + sb.append("hash=\""); + sb.append(payloadHash); + sb.append("\", "); + } + if (extra != null && extra.length() > 0) { + sb.append("ext=\""); + sb.append(escapeExtraHeaderAttribute(extra)); + sb.append("\", "); + } + sb.append("mac=\""); + sb.append(macString); + sb.append("\""); + + return new BasicHeader("Authorization", sb.toString()); + } + + /** + * Get the payload verification hash for the given request, if possible. + *
+ * Returns null if the request does not enclose an entity (is not an HTTP + * PATCH, POST, or PUT). Throws if the payload verification hash cannot be + * computed. + * + * @param request + * to compute hash for. + * @return verification hash, or null if the request does not enclose an entity. + * @throws IllegalArgumentException if the request does not enclose a valid non-null entity. + * @throws UnsupportedEncodingException + * @throws NoSuchAlgorithmException + * @throws IOException + */ + protected static String getPayloadHashString(HttpRequestBase request) + throws UnsupportedEncodingException, NoSuchAlgorithmException, IOException, IllegalArgumentException { + final boolean shouldComputePayloadHash = request instanceof HttpEntityEnclosingRequest; + if (!shouldComputePayloadHash) { + Logger.debug(LOG_TAG, "Not computing payload verification hash for non-enclosing request."); + return null; + } + if (!(request instanceof HttpEntityEnclosingRequest)) { + throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request without an entity"); + } + final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + if (entity == null) { + throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request with a null entity"); + } + return Base64.encodeBase64String(getPayloadHash(entity)); + } + + /** + * Escape the user-provided extra string for the ext="" header attribute. + *
+ * Hawk escapes the header ext="" attribute differently than it does the extra + * line in the normalized request string. + *
+ * See https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385. + * + * @param extra to escape. + * @return extra escaped for the ext="" header attribute. + */ + protected static String escapeExtraHeaderAttribute(String extra) { + return extra.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\""); + } + + /** + * Escape the user-provided extra string for inserting into the normalized + * request string. + *
+ * Hawk escapes the header ext="" attribute differently than it does the extra + * line in the normalized request string. + *
+ * See https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67. + * + * @param extra to escape. + * @return extra escaped for the normalized request string. + */ + protected static String escapeExtraString(String extra) { + return extra.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\n"); + } + + /** + * Return the content type with no parameters (pieces following ;). + * + * @param contentTypeHeader to interrogate. + * @return base content type. + */ + protected static String getBaseContentType(Header contentTypeHeader) { + if (contentTypeHeader == null) { + throw new IllegalArgumentException("contentTypeHeader must not be null."); + } + String contentType = contentTypeHeader.getValue(); + if (contentType == null) { + throw new IllegalArgumentException("contentTypeHeader value must not be null."); + } + int index = contentType.indexOf(";"); + if (index < 0) { + return contentType.trim(); + } + return contentType.substring(0, index).trim(); + } + + /** + * Generate the SHA-256 hash of a normalized Hawk payload generated from an + * HTTP entity. + *
+ * Warning: the entity must be repeatable. If it is not, this
+ * code throws an IllegalArgumentException
.
+ *
+ * This is under-specified; the code here was reverse engineered from the code + * at + * https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81. + * @param entity to normalize and hash. + * @return hash. + * @throws IllegalArgumentException if entity is not repeatable. + */ + protected static byte[] getPayloadHash(HttpEntity entity) throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException { + if (!entity.isRepeatable()) { + throw new IllegalArgumentException("entity must be repeatable"); + } + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(("hawk." + HAWK_HEADER_VERSION + ".payload\n").getBytes("UTF-8")); + digest.update(getBaseContentType(entity.getContentType()).getBytes("UTF-8")); + digest.update("\n".getBytes("UTF-8")); + InputStream stream = entity.getContent(); + try { + int numRead; + byte[] buffer = new byte[4096]; + while (-1 != (numRead = stream.read(buffer))) { + if (numRead > 0) { + digest.update(buffer, 0, numRead); + } + } + digest.update("\n".getBytes("UTF-8")); // Trailing newline is specified by Hawk. + return digest.digest(); + } finally { + stream.close(); + } + } + + /** + * Generate a normalized Hawk request string. This is under-specified; the + * code here was reverse engineered from the code at + * https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55. + *
+ * This method trusts its inputs to be valid.
+ */
+ protected static String getRequestString(HttpUriRequest request, String type, long timestamp, String nonce, String hash, String extra, String app, String dlg) {
+ String method = request.getMethod().toUpperCase(Locale.US);
+
+ URI uri = request.getURI();
+ String host = uri.getHost();
+
+ String path = uri.getRawPath();
+ if (uri.getRawQuery() != null) {
+ path += "?";
+ path += uri.getRawQuery();
+ }
+ if (uri.getRawFragment() != null) {
+ path += "#";
+ path += uri.getRawFragment();
+ }
+
+ int port = uri.getPort();
+ String scheme = uri.getScheme();
+ if (port != -1) {
+ } else if ("http".equalsIgnoreCase(scheme)) {
+ port = 80;
+ } else if ("https".equalsIgnoreCase(scheme)) {
+ port = 443;
+ } else {
+ throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + ".");
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("hawk.");
+ sb.append(HAWK_HEADER_VERSION);
+ sb.append('.');
+ sb.append(type);
+ sb.append('\n');
+ sb.append(timestamp);
+ sb.append('\n');
+ sb.append(nonce);
+ sb.append('\n');
+ sb.append(method);
+ sb.append('\n');
+ sb.append(path);
+ sb.append('\n');
+ sb.append(host);
+ sb.append('\n');
+ sb.append(port);
+ sb.append('\n');
+ if (hash != null) {
+ sb.append(hash);
+ }
+ sb.append("\n");
+ if (extra != null && extra.length() > 0) {
+ sb.append(escapeExtraString(extra));
+ }
+ sb.append("\n");
+ if (app != null) {
+ sb.append(app);
+ sb.append("\n");
+ if (dlg != null) {
+ sb.append(dlg);
+ }
+ sb.append("\n");
+ }
+
+ return sb.toString();
+ }
+
+ protected static byte[] hmacSha256(byte[] message, byte[] key)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+
+ SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA256_ALGORITHM);
+
+ Mac hasher = Mac.getInstance(HMAC_SHA256_ALGORITHM);
+ hasher.init(keySpec);
+ hasher.update(message);
+
+ return hasher.doFinal();
+ }
+
+ /**
+ * Sign a Hawk request string.
+ *
+ * @param requestString to sign.
+ * @param key as String
.
+ * @return signature as base-64 encoded string.
+ * @throws InvalidKeyException
+ * @throws NoSuchAlgorithmException
+ * @throws UnsupportedEncodingException
+ */
+ protected static String getSignature(byte[] requestString, byte[] key)
+ throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ return Base64.encodeBase64String(hmacSha256(requestString, key));
+ }
+}