diff -r 000000000000 -r 6474c204b198 mobile/android/base/sync/net/HMACAuthHeaderProvider.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mobile/android/base/sync/net/HMACAuthHeaderProvider.java Wed Dec 31 06:09:35 2014 +0100
@@ -0,0 +1,261 @@
+/* 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.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+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.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
+ * HMAC-SHA1-signed requests in the format expected by Mozilla Services
+ * identity-attached services and specified by the MAC Authentication spec, available at
+ * https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac.
+ *
+ * See https://wiki.mozilla.org/Services/Sagrada/ServiceClientFlow#Access.
+ */
+public class HMACAuthHeaderProvider implements AuthHeaderProvider {
+ public static final String LOG_TAG = "HMACAuthHeaderProvider";
+
+ public static final int NONCE_LENGTH_IN_BYTES = 8;
+
+ public static final String HMAC_SHA1_ALGORITHM = "hmacSHA1";
+
+ public final String identifier;
+ public final String key;
+
+ public HMACAuthHeaderProvider(String identifier, String key) {
+ // Validate identifier string. From the MAC Authentication spec:
+ // id = "id" "=" string-value
+ // string-value = ( <"> plain-string <"> ) / plain-string
+ // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
+ // We add quotes around the id string, so input identifier must be a plain-string.
+ if (identifier == null) {
+ throw new IllegalArgumentException("identifier must not be null.");
+ }
+ if (!isPlainString(identifier)) {
+ throw new IllegalArgumentException("identifier must be a plain-string.");
+ }
+
+ if (key == null) {
+ throw new IllegalArgumentException("key must not be null.");
+ }
+
+ this.identifier = identifier;
+ this.key = key;
+ }
+
+ @Override
+ public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
+ long timestamp = System.currentTimeMillis() / 1000;
+ String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES));
+ String extra = "";
+
+ try {
+ return getAuthHeader(request, context, client, timestamp, nonce, extra);
+ } catch (InvalidKeyException e) {
+ // We lie a little and make every exception a GeneralSecurityException.
+ throw new GeneralSecurityException(e);
+ } catch (UnsupportedEncodingException e) {
+ throw new GeneralSecurityException(e);
+ } catch (NoSuchAlgorithmException e) {
+ throw new GeneralSecurityException(e);
+ }
+ }
+
+ /**
+ * Test if input is a plain-string
.
+ *
+ * A plain-string is defined by the MAC Authentication spec as
+ * plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
.
+ *
+ * @param input
+ * as a String of "US-ASCII" bytes.
+ * @return true if input is a plain-string
; false otherwise.
+ * @throws UnsupportedEncodingException
+ */
+ protected static boolean isPlainString(String input) {
+ if (input == null || input.length() == 0) {
+ return false;
+ }
+
+ byte[] bytes;
+ try {
+ bytes = input.getBytes("US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // Should never happen.
+ Logger.warn(LOG_TAG, "Got exception in isPlainString; returning false.", e);
+ return false;
+ }
+
+ for (byte b : bytes) {
+ if ((0x20 <= b && b <= 0x21) || (0x23 <= b && b <= 0x5B) || (0x5D <= b && b <= 0x7E)) {
+ continue;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Helper function that generates an HTTP Authorization header given
+ * additional MAC Authentication specific data.
+ *
+ * @throws UnsupportedEncodingException
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeyException
+ */
+ protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client,
+ long timestamp, String nonce, String extra)
+ throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
+ // Validate timestamp. From the MAC Authentication spec:
+ // timestamp = 1*DIGIT
+ // This is equivalent to timestamp >= 0.
+ if (timestamp < 0) {
+ throw new IllegalArgumentException("timestamp must contain only [0-9].");
+ }
+
+ // Validate nonce string. From the MAC Authentication spec:
+ // nonce = "nonce" "=" string-value
+ // string-value = ( <"> plain-string <"> ) / plain-string
+ // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
+ // We add quotes around the nonce string, so input nonce must be a plain-string.
+ if (nonce == null) {
+ throw new IllegalArgumentException("nonce must not be null.");
+ }
+ if (nonce.length() == 0) {
+ throw new IllegalArgumentException("nonce must not be empty.");
+ }
+ if (!isPlainString(nonce)) {
+ throw new IllegalArgumentException("nonce must be a plain-string.");
+ }
+
+ // Validate extra string. From the MAC Authentication spec:
+ // ext = "ext" "=" string-value
+ // string-value = ( <"> plain-string <"> ) / plain-string
+ // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
+ // We add quotes around the extra string, so input extra must be a plain-string.
+ // We break the spec by allowing ext to be an empty string, i.e. to match 0*(...).
+ if (extra == null) {
+ throw new IllegalArgumentException("extra must not be null.");
+ }
+ if (extra.length() > 0 && !isPlainString(extra)) {
+ throw new IllegalArgumentException("extra must be a plain-string.");
+ }
+
+ String requestString = getRequestString(request, timestamp, nonce, extra);
+ String macString = getSignature(requestString, this.key);
+
+ String h = "MAC id=\"" + this.identifier + "\", " +
+ "ts=\"" + timestamp + "\", " +
+ "nonce=\"" + nonce + "\", " +
+ "mac=\"" + macString + "\"";
+
+ if (extra != null) {
+ h += ", ext=\"" + extra + "\"";
+ }
+
+ Header header = new BasicHeader("Authorization", h);
+
+ return header;
+ }
+
+ protected static byte[] sha1(byte[] message, byte[] key)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+
+ SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA1_ALGORITHM);
+
+ Mac hasher = Mac.getInstance(HMAC_SHA1_ALGORITHM);
+ hasher.init(keySpec);
+ hasher.update(message);
+
+ byte[] hmac = hasher.doFinal();
+
+ return hmac;
+ }
+
+ /**
+ * Sign an HMAC 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(String requestString, String key)
+ throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ String macString = Base64.encodeBase64String(sha1(requestString.getBytes("UTF-8"), key.getBytes("UTF-8")));
+
+ return macString;
+ }
+
+ /**
+ * Generate an HMAC request string.
+ *
+ * This method trusts its inputs to be valid as per the MAC Authentication spec. + * + * @param request HTTP request. + * @param timestamp to use. + * @param nonce to use. + * @param extra to use. + * @return request string. + */ + protected static String getRequestString(HttpUriRequest request, long timestamp, String nonce, String extra) { + String method = request.getMethod().toUpperCase(); + + 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 + "."); + } + + String requestString = timestamp + "\n" + + nonce + "\n" + + method + "\n" + + path + "\n" + + host + "\n" + + port + "\n" + + extra + "\n"; + + return requestString; + } +}