michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: package org.mozilla.gecko.sync.net;
michael@0:
michael@0: import java.io.UnsupportedEncodingException;
michael@0: import java.net.URI;
michael@0: import java.security.GeneralSecurityException;
michael@0: import java.security.InvalidKeyException;
michael@0: import java.security.NoSuchAlgorithmException;
michael@0:
michael@0: import javax.crypto.Mac;
michael@0: import javax.crypto.spec.SecretKeySpec;
michael@0:
michael@0: import org.mozilla.apache.commons.codec.binary.Base64;
michael@0: import org.mozilla.gecko.background.common.log.Logger;
michael@0: import org.mozilla.gecko.sync.Utils;
michael@0:
michael@0: import ch.boye.httpclientandroidlib.Header;
michael@0: import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
michael@0: import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
michael@0: import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
michael@0: import ch.boye.httpclientandroidlib.message.BasicHeader;
michael@0: import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
michael@0:
michael@0: /**
michael@0: * An AuthHeaderProvider
that returns an Authorization header for
michael@0: * HMAC-SHA1-signed requests in the format expected by Mozilla Services
michael@0: * identity-attached services and specified by the MAC Authentication spec, available at
michael@0: * https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac.
michael@0: *
michael@0: * See https://wiki.mozilla.org/Services/Sagrada/ServiceClientFlow#Access.
michael@0: */
michael@0: public class HMACAuthHeaderProvider implements AuthHeaderProvider {
michael@0: public static final String LOG_TAG = "HMACAuthHeaderProvider";
michael@0:
michael@0: public static final int NONCE_LENGTH_IN_BYTES = 8;
michael@0:
michael@0: public static final String HMAC_SHA1_ALGORITHM = "hmacSHA1";
michael@0:
michael@0: public final String identifier;
michael@0: public final String key;
michael@0:
michael@0: public HMACAuthHeaderProvider(String identifier, String key) {
michael@0: // Validate identifier string. From the MAC Authentication spec:
michael@0: // id = "id" "=" string-value
michael@0: // string-value = ( <"> plain-string <"> ) / plain-string
michael@0: // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
michael@0: // We add quotes around the id string, so input identifier must be a plain-string.
michael@0: if (identifier == null) {
michael@0: throw new IllegalArgumentException("identifier must not be null.");
michael@0: }
michael@0: if (!isPlainString(identifier)) {
michael@0: throw new IllegalArgumentException("identifier must be a plain-string.");
michael@0: }
michael@0:
michael@0: if (key == null) {
michael@0: throw new IllegalArgumentException("key must not be null.");
michael@0: }
michael@0:
michael@0: this.identifier = identifier;
michael@0: this.key = key;
michael@0: }
michael@0:
michael@0: @Override
michael@0: public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
michael@0: long timestamp = System.currentTimeMillis() / 1000;
michael@0: String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES));
michael@0: String extra = "";
michael@0:
michael@0: try {
michael@0: return getAuthHeader(request, context, client, timestamp, nonce, extra);
michael@0: } catch (InvalidKeyException e) {
michael@0: // We lie a little and make every exception a GeneralSecurityException.
michael@0: throw new GeneralSecurityException(e);
michael@0: } catch (UnsupportedEncodingException e) {
michael@0: throw new GeneralSecurityException(e);
michael@0: } catch (NoSuchAlgorithmException e) {
michael@0: throw new GeneralSecurityException(e);
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Test if input is a plain-string
.
michael@0: *
michael@0: * A plain-string is defined by the MAC Authentication spec as
michael@0: * plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
.
michael@0: *
michael@0: * @param input
michael@0: * as a String of "US-ASCII" bytes.
michael@0: * @return true if input is a plain-string
; false otherwise.
michael@0: * @throws UnsupportedEncodingException
michael@0: */
michael@0: protected static boolean isPlainString(String input) {
michael@0: if (input == null || input.length() == 0) {
michael@0: return false;
michael@0: }
michael@0:
michael@0: byte[] bytes;
michael@0: try {
michael@0: bytes = input.getBytes("US-ASCII");
michael@0: } catch (UnsupportedEncodingException e) {
michael@0: // Should never happen.
michael@0: Logger.warn(LOG_TAG, "Got exception in isPlainString; returning false.", e);
michael@0: return false;
michael@0: }
michael@0:
michael@0: for (byte b : bytes) {
michael@0: if ((0x20 <= b && b <= 0x21) || (0x23 <= b && b <= 0x5B) || (0x5D <= b && b <= 0x7E)) {
michael@0: continue;
michael@0: }
michael@0: return false;
michael@0: }
michael@0:
michael@0: return true;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Helper function that generates an HTTP Authorization header given
michael@0: * additional MAC Authentication specific data.
michael@0: *
michael@0: * @throws UnsupportedEncodingException
michael@0: * @throws NoSuchAlgorithmException
michael@0: * @throws InvalidKeyException
michael@0: */
michael@0: protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client,
michael@0: long timestamp, String nonce, String extra)
michael@0: throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
michael@0: // Validate timestamp. From the MAC Authentication spec:
michael@0: // timestamp = 1*DIGIT
michael@0: // This is equivalent to timestamp >= 0.
michael@0: if (timestamp < 0) {
michael@0: throw new IllegalArgumentException("timestamp must contain only [0-9].");
michael@0: }
michael@0:
michael@0: // Validate nonce string. From the MAC Authentication spec:
michael@0: // nonce = "nonce" "=" string-value
michael@0: // string-value = ( <"> plain-string <"> ) / plain-string
michael@0: // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
michael@0: // We add quotes around the nonce string, so input nonce must be a plain-string.
michael@0: if (nonce == null) {
michael@0: throw new IllegalArgumentException("nonce must not be null.");
michael@0: }
michael@0: if (nonce.length() == 0) {
michael@0: throw new IllegalArgumentException("nonce must not be empty.");
michael@0: }
michael@0: if (!isPlainString(nonce)) {
michael@0: throw new IllegalArgumentException("nonce must be a plain-string.");
michael@0: }
michael@0:
michael@0: // Validate extra string. From the MAC Authentication spec:
michael@0: // ext = "ext" "=" string-value
michael@0: // string-value = ( <"> plain-string <"> ) / plain-string
michael@0: // plain-string = 1*( %x20-21 / %x23-5B / %x5D-7E )
michael@0: // We add quotes around the extra string, so input extra must be a plain-string.
michael@0: // We break the spec by allowing ext to be an empty string, i.e. to match 0*(...).
michael@0: if (extra == null) {
michael@0: throw new IllegalArgumentException("extra must not be null.");
michael@0: }
michael@0: if (extra.length() > 0 && !isPlainString(extra)) {
michael@0: throw new IllegalArgumentException("extra must be a plain-string.");
michael@0: }
michael@0:
michael@0: String requestString = getRequestString(request, timestamp, nonce, extra);
michael@0: String macString = getSignature(requestString, this.key);
michael@0:
michael@0: String h = "MAC id=\"" + this.identifier + "\", " +
michael@0: "ts=\"" + timestamp + "\", " +
michael@0: "nonce=\"" + nonce + "\", " +
michael@0: "mac=\"" + macString + "\"";
michael@0:
michael@0: if (extra != null) {
michael@0: h += ", ext=\"" + extra + "\"";
michael@0: }
michael@0:
michael@0: Header header = new BasicHeader("Authorization", h);
michael@0:
michael@0: return header;
michael@0: }
michael@0:
michael@0: protected static byte[] sha1(byte[] message, byte[] key)
michael@0: throws NoSuchAlgorithmException, InvalidKeyException {
michael@0:
michael@0: SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA1_ALGORITHM);
michael@0:
michael@0: Mac hasher = Mac.getInstance(HMAC_SHA1_ALGORITHM);
michael@0: hasher.init(keySpec);
michael@0: hasher.update(message);
michael@0:
michael@0: byte[] hmac = hasher.doFinal();
michael@0:
michael@0: return hmac;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Sign an HMAC request string.
michael@0: *
michael@0: * @param requestString to sign.
michael@0: * @param key as String
.
michael@0: * @return signature as base-64 encoded string.
michael@0: * @throws InvalidKeyException
michael@0: * @throws NoSuchAlgorithmException
michael@0: * @throws UnsupportedEncodingException
michael@0: */
michael@0: protected static String getSignature(String requestString, String key)
michael@0: throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
michael@0: String macString = Base64.encodeBase64String(sha1(requestString.getBytes("UTF-8"), key.getBytes("UTF-8")));
michael@0:
michael@0: return macString;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Generate an HMAC request string.
michael@0: *
michael@0: * This method trusts its inputs to be valid as per the MAC Authentication spec. michael@0: * michael@0: * @param request HTTP request. michael@0: * @param timestamp to use. michael@0: * @param nonce to use. michael@0: * @param extra to use. michael@0: * @return request string. michael@0: */ michael@0: protected static String getRequestString(HttpUriRequest request, long timestamp, String nonce, String extra) { michael@0: String method = request.getMethod().toUpperCase(); michael@0: michael@0: URI uri = request.getURI(); michael@0: String host = uri.getHost(); michael@0: michael@0: String path = uri.getRawPath(); michael@0: if (uri.getRawQuery() != null) { michael@0: path += "?"; michael@0: path += uri.getRawQuery(); michael@0: } michael@0: if (uri.getRawFragment() != null) { michael@0: path += "#"; michael@0: path += uri.getRawFragment(); michael@0: } michael@0: michael@0: int port = uri.getPort(); michael@0: String scheme = uri.getScheme(); michael@0: if (port != -1) { michael@0: } else if ("http".equalsIgnoreCase(scheme)) { michael@0: port = 80; michael@0: } else if ("https".equalsIgnoreCase(scheme)) { michael@0: port = 443; michael@0: } else { michael@0: throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + "."); michael@0: } michael@0: michael@0: String requestString = timestamp + "\n" + michael@0: nonce + "\n" + michael@0: method + "\n" + michael@0: path + "\n" + michael@0: host + "\n" + michael@0: port + "\n" + michael@0: extra + "\n"; michael@0: michael@0: return requestString; michael@0: } michael@0: }