mobile/android/base/sync/net/HawkAuthHeaderProvider.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.sync.net;
     7 import java.io.IOException;
     8 import java.io.InputStream;
     9 import java.io.UnsupportedEncodingException;
    10 import java.net.URI;
    11 import java.security.GeneralSecurityException;
    12 import java.security.InvalidKeyException;
    13 import java.security.MessageDigest;
    14 import java.security.NoSuchAlgorithmException;
    15 import java.util.Locale;
    17 import javax.crypto.Mac;
    18 import javax.crypto.spec.SecretKeySpec;
    20 import org.mozilla.apache.commons.codec.binary.Base64;
    21 import org.mozilla.gecko.background.common.log.Logger;
    22 import org.mozilla.gecko.sync.Utils;
    24 import ch.boye.httpclientandroidlib.Header;
    25 import ch.boye.httpclientandroidlib.HttpEntity;
    26 import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
    27 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
    28 import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
    29 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
    30 import ch.boye.httpclientandroidlib.message.BasicHeader;
    31 import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
    33 /**
    34  * An <code>AuthHeaderProvider</code> that returns an Authorization header for
    35  * Hawk: <a href="https://github.com/hueniverse/hawk">https://github.com/hueniverse/hawk</a>.
    36  *
    37  * Hawk is an HTTP authentication scheme using a message authentication code
    38  * (MAC) algorithm to provide partial HTTP request cryptographic verification.
    39  * Hawk is the successor to the HMAC authentication scheme.
    40  */
    41 public class HawkAuthHeaderProvider implements AuthHeaderProvider {
    42   public static final String LOG_TAG = HawkAuthHeaderProvider.class.getSimpleName();
    44   public static final int HAWK_HEADER_VERSION = 1;
    46   protected static final int NONCE_LENGTH_IN_BYTES = 8;
    47   protected static final String HMAC_SHA256_ALGORITHM = "hmacSHA256";
    49   protected final String id;
    50   protected final byte[] key;
    51   protected final boolean includePayloadHash;
    52   protected final long skewSeconds;
    54   /**
    55    * Create a Hawk Authorization header provider.
    56    * <p>
    57    * Hawk specifies no mechanism by which a client receives an
    58    * identifier-and-key pair from the server.
    59    * <p>
    60    * Hawk requests can include a payload verification hash with requests that
    61    * enclose an entity (PATCH, POST, and PUT requests).  <b>You should default
    62    * to including the payload verification hash<b> unless you have a good reason
    63    * not to -- the server can always ignore payload verification hashes provided
    64    * by the client.
    65    *
    66    * @param id
    67    *          to name requests with.
    68    * @param key
    69    *          to sign request with.
    70    *
    71    * @param includePayloadHash
    72    *          true if payload verification hash should be included in signed
    73    *          request header. See <a href="https://github.com/hueniverse/hawk#payload-validation">https://github.com/hueniverse/hawk#payload-validation</a>.
    74    *
    75    * @param skewSeconds
    76    *          a number of seconds by which to skew the current time when
    77    *          computing a header.
    78    */
    79   public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash, long skewSeconds) {
    80     if (id == null) {
    81       throw new IllegalArgumentException("id must not be null");
    82     }
    83     if (key == null) {
    84       throw new IllegalArgumentException("key must not be null");
    85     }
    86     this.id = id;
    87     this.key = key;
    88     this.includePayloadHash = includePayloadHash;
    89     this.skewSeconds = skewSeconds;
    90   }
    92   /**
    93    * @return the current time in milliseconds.
    94    */
    95   @SuppressWarnings("static-method")
    96   protected long now() {
    97     return System.currentTimeMillis();
    98   }
   100   /**
   101    * @return the current time in seconds, adjusted for skew. This should
   102    *         approximate the server's timestamp.
   103    */
   104   protected long getTimestampSeconds() {
   105     return (now() / 1000) + skewSeconds;
   106   }
   108   @Override
   109   public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
   110     long timestamp = getTimestampSeconds();
   111     String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES));
   112     String extra = "";
   114     try {
   115       return getAuthHeader(request, context, client, timestamp, nonce, extra, this.includePayloadHash);
   116     } catch (Exception e) {
   117       // We lie a little and make every exception a GeneralSecurityException.
   118       throw new GeneralSecurityException(e);
   119     }
   120   }
   122   /**
   123    * Helper function that generates an HTTP Authorization: Hawk header given
   124    * additional Hawk specific data.
   125    *
   126    * @throws NoSuchAlgorithmException
   127    * @throws InvalidKeyException
   128    * @throws IOException
   129    */
   130   protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client,
   131       long timestamp, String nonce, String extra, boolean includePayloadHash)
   132           throws InvalidKeyException, NoSuchAlgorithmException, IOException {
   133     if (timestamp < 0) {
   134       throw new IllegalArgumentException("timestamp must contain only [0-9].");
   135     }
   137     if (nonce == null) {
   138       throw new IllegalArgumentException("nonce must not be null.");
   139     }
   140     if (nonce.length() == 0) {
   141       throw new IllegalArgumentException("nonce must not be empty.");
   142     }
   144     String payloadHash = null;
   145     if (includePayloadHash) {
   146       payloadHash = getPayloadHashString(request);
   147     } else {
   148       Logger.debug(LOG_TAG, "Configured to not include payload hash for this request.");
   149     }
   151     String app = null;
   152     String dlg = null;
   153     String requestString = getRequestString(request, "header", timestamp, nonce, payloadHash, extra, app, dlg);
   154     String macString = getSignature(requestString.getBytes("UTF-8"), this.key);
   156     StringBuilder sb = new StringBuilder();
   157     sb.append("Hawk id=\"");
   158     sb.append(this.id);
   159     sb.append("\", ");
   160     sb.append("ts=\"");
   161     sb.append(timestamp);
   162     sb.append("\", ");
   163     sb.append("nonce=\"");
   164     sb.append(nonce);
   165     sb.append("\", ");
   166     if (payloadHash != null) {
   167       sb.append("hash=\"");
   168       sb.append(payloadHash);
   169       sb.append("\", ");
   170     }
   171     if (extra != null && extra.length() > 0) {
   172       sb.append("ext=\"");
   173       sb.append(escapeExtraHeaderAttribute(extra));
   174       sb.append("\", ");
   175     }
   176     sb.append("mac=\"");
   177     sb.append(macString);
   178     sb.append("\"");
   180     return new BasicHeader("Authorization", sb.toString());
   181   }
   183   /**
   184    * Get the payload verification hash for the given request, if possible.
   185    * <p>
   186    * Returns null if the request does not enclose an entity (is not an HTTP
   187    * PATCH, POST, or PUT). Throws if the payload verification hash cannot be
   188    * computed.
   189    *
   190    * @param request
   191    *          to compute hash for.
   192    * @return verification hash, or null if the request does not enclose an entity.
   193    * @throws IllegalArgumentException if the request does not enclose a valid non-null entity.
   194    * @throws UnsupportedEncodingException
   195    * @throws NoSuchAlgorithmException
   196    * @throws IOException
   197    */
   198   protected static String getPayloadHashString(HttpRequestBase request)
   199       throws UnsupportedEncodingException, NoSuchAlgorithmException, IOException, IllegalArgumentException {
   200     final boolean shouldComputePayloadHash = request instanceof HttpEntityEnclosingRequest;
   201     if (!shouldComputePayloadHash) {
   202       Logger.debug(LOG_TAG, "Not computing payload verification hash for non-enclosing request.");
   203       return null;
   204     }
   205     if (!(request instanceof HttpEntityEnclosingRequest)) {
   206       throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request without an entity");
   207     }
   208     final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
   209     if (entity == null) {
   210       throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request with a null entity");
   211     }
   212     return Base64.encodeBase64String(getPayloadHash(entity));
   213   }
   215   /**
   216    * Escape the user-provided extra string for the ext="" header attribute.
   217    * <p>
   218    * Hawk escapes the header ext="" attribute differently than it does the extra
   219    * line in the normalized request string.
   220    * <p>
   221    * 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>.
   222    *
   223    * @param extra to escape.
   224    * @return extra escaped for the ext="" header attribute.
   225    */
   226   protected static String escapeExtraHeaderAttribute(String extra) {
   227     return extra.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\"");
   228   }
   230   /**
   231    * Escape the user-provided extra string for inserting into the normalized
   232    * request string.
   233    * <p>
   234    * Hawk escapes the header ext="" attribute differently than it does the extra
   235    * line in the normalized request string.
   236    * <p>
   237    * 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>.
   238    *
   239    * @param extra to escape.
   240    * @return extra escaped for the normalized request string.
   241    */
   242   protected static String escapeExtraString(String extra) {
   243     return extra.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\n");
   244   }
   246   /**
   247    * Return the content type with no parameters (pieces following ;).
   248    *
   249    * @param contentTypeHeader to interrogate.
   250    * @return base content type.
   251    */
   252   protected static String getBaseContentType(Header contentTypeHeader) {
   253     if (contentTypeHeader == null) {
   254       throw new IllegalArgumentException("contentTypeHeader must not be null.");
   255     }
   256     String contentType = contentTypeHeader.getValue();
   257     if (contentType == null) {
   258       throw new IllegalArgumentException("contentTypeHeader value must not be null.");
   259     }
   260     int index = contentType.indexOf(";");
   261     if (index < 0) {
   262       return contentType.trim();
   263     }
   264     return contentType.substring(0, index).trim();
   265   }
   267   /**
   268    * Generate the SHA-256 hash of a normalized Hawk payload generated from an
   269    * HTTP entity.
   270    * <p>
   271    * <b>Warning:</b> the entity <b>must</b> be repeatable.  If it is not, this
   272    * code throws an <code>IllegalArgumentException</code>.
   273    * <p>
   274    * This is under-specified; the code here was reverse engineered from the code
   275    * at
   276    * <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>.
   277    * @param entity to normalize and hash.
   278    * @return hash.
   279    * @throws IllegalArgumentException if entity is not repeatable.
   280    */
   281   protected static byte[] getPayloadHash(HttpEntity entity) throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException {
   282     if (!entity.isRepeatable()) {
   283       throw new IllegalArgumentException("entity must be repeatable");
   284     }
   285     final MessageDigest digest = MessageDigest.getInstance("SHA-256");
   286     digest.update(("hawk." + HAWK_HEADER_VERSION + ".payload\n").getBytes("UTF-8"));
   287     digest.update(getBaseContentType(entity.getContentType()).getBytes("UTF-8"));
   288     digest.update("\n".getBytes("UTF-8"));
   289     InputStream stream = entity.getContent();
   290     try {
   291       int numRead;
   292       byte[] buffer = new byte[4096];
   293       while (-1 != (numRead = stream.read(buffer))) {
   294         if (numRead > 0) {
   295           digest.update(buffer, 0, numRead);
   296         }
   297       }
   298       digest.update("\n".getBytes("UTF-8")); // Trailing newline is specified by Hawk.
   299       return digest.digest();
   300     } finally {
   301       stream.close();
   302     }
   303   }
   305   /**
   306    * Generate a normalized Hawk request string. This is under-specified; the
   307    * code here was reverse engineered from the code at
   308    * <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>.
   309    * <p>
   310    * This method trusts its inputs to be valid.
   311    */
   312   protected static String getRequestString(HttpUriRequest request, String type, long timestamp, String nonce, String hash, String extra, String app, String dlg) {
   313     String method = request.getMethod().toUpperCase(Locale.US);
   315     URI uri = request.getURI();
   316     String host = uri.getHost();
   318     String path = uri.getRawPath();
   319     if (uri.getRawQuery() != null) {
   320       path += "?";
   321       path += uri.getRawQuery();
   322     }
   323     if (uri.getRawFragment() != null) {
   324       path += "#";
   325       path += uri.getRawFragment();
   326     }
   328     int port = uri.getPort();
   329     String scheme = uri.getScheme();
   330     if (port != -1) {
   331     } else if ("http".equalsIgnoreCase(scheme)) {
   332       port = 80;
   333     } else if ("https".equalsIgnoreCase(scheme)) {
   334       port = 443;
   335     } else {
   336       throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + ".");
   337     }
   339     StringBuilder sb = new StringBuilder();
   340     sb.append("hawk.");
   341     sb.append(HAWK_HEADER_VERSION);
   342     sb.append('.');
   343     sb.append(type);
   344     sb.append('\n');
   345     sb.append(timestamp);
   346     sb.append('\n');
   347     sb.append(nonce);
   348     sb.append('\n');
   349     sb.append(method);
   350     sb.append('\n');
   351     sb.append(path);
   352     sb.append('\n');
   353     sb.append(host);
   354     sb.append('\n');
   355     sb.append(port);
   356     sb.append('\n');
   357     if (hash != null) {
   358       sb.append(hash);
   359     }
   360     sb.append("\n");
   361     if (extra != null && extra.length() > 0) {
   362       sb.append(escapeExtraString(extra));
   363     }
   364     sb.append("\n");
   365     if (app != null) {
   366       sb.append(app);
   367       sb.append("\n");
   368       if (dlg != null) {
   369         sb.append(dlg);
   370       }
   371       sb.append("\n");
   372     }
   374     return sb.toString();
   375   }
   377   protected static byte[] hmacSha256(byte[] message, byte[] key)
   378       throws NoSuchAlgorithmException, InvalidKeyException {
   380     SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA256_ALGORITHM);
   382     Mac hasher = Mac.getInstance(HMAC_SHA256_ALGORITHM);
   383     hasher.init(keySpec);
   384     hasher.update(message);
   386     return hasher.doFinal();
   387   }
   389   /**
   390    * Sign a Hawk request string.
   391    *
   392    * @param requestString to sign.
   393    * @param key as <code>String</code>.
   394    * @return signature as base-64 encoded string.
   395    * @throws InvalidKeyException
   396    * @throws NoSuchAlgorithmException
   397    * @throws UnsupportedEncodingException
   398    */
   399   protected static String getSignature(byte[] requestString, byte[] key)
   400       throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
   401     return Base64.encodeBase64String(hmacSha256(requestString, key));
   402   }
   403 }

mercurial