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; + } +}