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: }