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

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/sync/net/HMACAuthHeaderProvider.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,261 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko.sync.net;
     1.9 +
    1.10 +import java.io.UnsupportedEncodingException;
    1.11 +import java.net.URI;
    1.12 +import java.security.GeneralSecurityException;
    1.13 +import java.security.InvalidKeyException;
    1.14 +import java.security.NoSuchAlgorithmException;
    1.15 +
    1.16 +import javax.crypto.Mac;
    1.17 +import javax.crypto.spec.SecretKeySpec;
    1.18 +
    1.19 +import org.mozilla.apache.commons.codec.binary.Base64;
    1.20 +import org.mozilla.gecko.background.common.log.Logger;
    1.21 +import org.mozilla.gecko.sync.Utils;
    1.22 +
    1.23 +import ch.boye.httpclientandroidlib.Header;
    1.24 +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
    1.25 +import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
    1.26 +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
    1.27 +import ch.boye.httpclientandroidlib.message.BasicHeader;
    1.28 +import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
    1.29 +
    1.30 +/**
    1.31 + * An <code>AuthHeaderProvider</code> that returns an Authorization header for
    1.32 + * HMAC-SHA1-signed requests in the format expected by Mozilla Services
    1.33 + * identity-attached services and specified by the MAC Authentication spec, available at
    1.34 + * <a href="https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac">https://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac</a>.
    1.35 + * <p>
    1.36 + * See <a href="https://wiki.mozilla.org/Services/Sagrada/ServiceClientFlow#Access">https://wiki.mozilla.org/Services/Sagrada/ServiceClientFlow#Access</a>.
    1.37 + */
    1.38 +public class HMACAuthHeaderProvider implements AuthHeaderProvider {
    1.39 +  public static final String LOG_TAG = "HMACAuthHeaderProvider";
    1.40 +
    1.41 +  public static final int NONCE_LENGTH_IN_BYTES = 8;
    1.42 +
    1.43 +  public static final String HMAC_SHA1_ALGORITHM = "hmacSHA1";
    1.44 +
    1.45 +  public final String identifier;
    1.46 +  public final String key;
    1.47 +
    1.48 +  public HMACAuthHeaderProvider(String identifier, String key) {
    1.49 +    // Validate identifier string.  From the MAC Authentication spec:
    1.50 +    // id             = "id" "=" string-value
    1.51 +    // string-value   = ( <"> plain-string <"> ) / plain-string
    1.52 +    // plain-string   = 1*( %x20-21 / %x23-5B / %x5D-7E )
    1.53 +    // We add quotes around the id string, so input identifier must be a plain-string.
    1.54 +    if (identifier == null) {
    1.55 +      throw new IllegalArgumentException("identifier must not be null.");
    1.56 +    }
    1.57 +    if (!isPlainString(identifier)) {
    1.58 +      throw new IllegalArgumentException("identifier must be a plain-string.");
    1.59 +    }
    1.60 +
    1.61 +    if (key == null) {
    1.62 +      throw new IllegalArgumentException("key must not be null.");
    1.63 +    }
    1.64 +
    1.65 +    this.identifier = identifier;
    1.66 +    this.key = key;
    1.67 +  }
    1.68 +
    1.69 +  @Override
    1.70 +  public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
    1.71 +    long timestamp = System.currentTimeMillis() / 1000;
    1.72 +    String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES));
    1.73 +    String extra = "";
    1.74 +
    1.75 +    try {
    1.76 +      return getAuthHeader(request, context, client, timestamp, nonce, extra);
    1.77 +    } catch (InvalidKeyException e) {
    1.78 +      // We lie a little and make every exception a GeneralSecurityException.
    1.79 +      throw new GeneralSecurityException(e);
    1.80 +    } catch (UnsupportedEncodingException e) {
    1.81 +      throw new GeneralSecurityException(e);
    1.82 +    } catch (NoSuchAlgorithmException e) {
    1.83 +      throw new GeneralSecurityException(e);
    1.84 +    }
    1.85 +  }
    1.86 +
    1.87 +  /**
    1.88 +   * Test if input is a <code>plain-string</code>.
    1.89 +   * <p>
    1.90 +   * A plain-string is defined by the MAC Authentication spec as
    1.91 +   * <code>plain-string   = 1*( %x20-21 / %x23-5B / %x5D-7E )</code>.
    1.92 +   *
    1.93 +   * @param input
    1.94 +   *          as a String of "US-ASCII" bytes.
    1.95 +   * @return true if input is a <code>plain-string</code>; false otherwise.
    1.96 +   * @throws UnsupportedEncodingException
    1.97 +   */
    1.98 +  protected static boolean isPlainString(String input) {
    1.99 +    if (input == null || input.length() == 0) {
   1.100 +      return false;
   1.101 +    }
   1.102 +
   1.103 +    byte[] bytes;
   1.104 +    try {
   1.105 +      bytes = input.getBytes("US-ASCII");
   1.106 +    } catch (UnsupportedEncodingException e) {
   1.107 +      // Should never happen.
   1.108 +      Logger.warn(LOG_TAG, "Got exception in isPlainString; returning false.", e);
   1.109 +      return false;
   1.110 +    }
   1.111 +
   1.112 +    for (byte b : bytes) {
   1.113 +      if ((0x20 <= b && b <= 0x21) || (0x23 <= b && b <= 0x5B) || (0x5D <= b && b <= 0x7E)) {
   1.114 +        continue;
   1.115 +      }
   1.116 +      return false;
   1.117 +    }
   1.118 +
   1.119 +    return true;
   1.120 +  }
   1.121 +
   1.122 +  /**
   1.123 +   * Helper function that generates an HTTP Authorization header given
   1.124 +   * additional MAC Authentication specific data.
   1.125 +   *
   1.126 +   * @throws UnsupportedEncodingException
   1.127 +   * @throws NoSuchAlgorithmException 
   1.128 +   * @throws InvalidKeyException 
   1.129 +   */
   1.130 +  protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client,
   1.131 +      long timestamp, String nonce, String extra)
   1.132 +      throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
   1.133 +    // Validate timestamp.  From the MAC Authentication spec:
   1.134 +    // timestamp      = 1*DIGIT
   1.135 +    // This is equivalent to timestamp >= 0.
   1.136 +    if (timestamp < 0) {
   1.137 +      throw new IllegalArgumentException("timestamp must contain only [0-9].");
   1.138 +    }
   1.139 +
   1.140 +    // Validate nonce string.  From the MAC Authentication spec:
   1.141 +    // nonce          = "nonce" "=" string-value
   1.142 +    // string-value   = ( <"> plain-string <"> ) / plain-string
   1.143 +    // plain-string   = 1*( %x20-21 / %x23-5B / %x5D-7E )
   1.144 +    // We add quotes around the nonce string, so input nonce must be a plain-string.
   1.145 +    if (nonce == null) {
   1.146 +      throw new IllegalArgumentException("nonce must not be null.");
   1.147 +    }
   1.148 +    if (nonce.length() == 0) {
   1.149 +      throw new IllegalArgumentException("nonce must not be empty.");
   1.150 +    }
   1.151 +    if (!isPlainString(nonce)) {
   1.152 +      throw new IllegalArgumentException("nonce must be a plain-string.");
   1.153 +    }
   1.154 +
   1.155 +    // Validate extra string.  From the MAC Authentication spec:
   1.156 +    // ext            = "ext" "=" string-value
   1.157 +    // string-value   = ( <"> plain-string <"> ) / plain-string
   1.158 +    // plain-string   = 1*( %x20-21 / %x23-5B / %x5D-7E )
   1.159 +    // We add quotes around the extra string, so input extra must be a plain-string.
   1.160 +    // We break the spec by allowing ext to be an empty string, i.e. to match 0*(...).
   1.161 +    if (extra == null) {
   1.162 +      throw new IllegalArgumentException("extra must not be null.");
   1.163 +    }
   1.164 +    if (extra.length() > 0 && !isPlainString(extra)) {
   1.165 +      throw new IllegalArgumentException("extra must be a plain-string.");
   1.166 +    }
   1.167 +
   1.168 +    String requestString = getRequestString(request, timestamp, nonce, extra);
   1.169 +    String macString = getSignature(requestString, this.key);
   1.170 +
   1.171 +    String h = "MAC id=\"" + this.identifier + "\", " +
   1.172 +               "ts=\""     + timestamp       + "\", " +
   1.173 +               "nonce=\""  + nonce           + "\", " +
   1.174 +               "mac=\""    + macString       + "\"";
   1.175 +
   1.176 +    if (extra != null) {
   1.177 +      h += ", ext=\"" + extra + "\"";
   1.178 +    }
   1.179 +
   1.180 +    Header header = new BasicHeader("Authorization", h);
   1.181 +
   1.182 +    return header;
   1.183 +  }
   1.184 +
   1.185 +  protected static byte[] sha1(byte[] message, byte[] key)
   1.186 +      throws NoSuchAlgorithmException, InvalidKeyException {
   1.187 +
   1.188 +    SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA1_ALGORITHM);
   1.189 +
   1.190 +    Mac hasher = Mac.getInstance(HMAC_SHA1_ALGORITHM);
   1.191 +    hasher.init(keySpec);
   1.192 +    hasher.update(message);
   1.193 +
   1.194 +    byte[] hmac = hasher.doFinal();
   1.195 +
   1.196 +    return hmac;
   1.197 +  }
   1.198 +
   1.199 +  /**
   1.200 +   * Sign an HMAC request string.
   1.201 +   *
   1.202 +   * @param requestString to sign.
   1.203 +   * @param key as <code>String</code>.
   1.204 +   * @return signature as base-64 encoded string.
   1.205 +   * @throws InvalidKeyException
   1.206 +   * @throws NoSuchAlgorithmException
   1.207 +   * @throws UnsupportedEncodingException
   1.208 +   */
   1.209 +  protected static String getSignature(String requestString, String key)
   1.210 +      throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
   1.211 +    String macString = Base64.encodeBase64String(sha1(requestString.getBytes("UTF-8"), key.getBytes("UTF-8")));
   1.212 +
   1.213 +    return macString;
   1.214 +  }
   1.215 +
   1.216 +  /**
   1.217 +   * Generate an HMAC request string.
   1.218 +   * <p>
   1.219 +   * This method trusts its inputs to be valid as per the MAC Authentication spec.
   1.220 +   *
   1.221 +   * @param request HTTP request.
   1.222 +   * @param timestamp to use.
   1.223 +   * @param nonce to use.
   1.224 +   * @param extra to use.
   1.225 +   * @return request string.
   1.226 +   */
   1.227 +  protected static String getRequestString(HttpUriRequest request, long timestamp, String nonce, String extra) {
   1.228 +    String method = request.getMethod().toUpperCase();
   1.229 +
   1.230 +    URI uri = request.getURI();
   1.231 +    String host = uri.getHost();
   1.232 +
   1.233 +    String path = uri.getRawPath();
   1.234 +    if (uri.getRawQuery() != null) {
   1.235 +      path += "?";
   1.236 +      path += uri.getRawQuery();
   1.237 +    }
   1.238 +    if (uri.getRawFragment() != null) {
   1.239 +      path += "#";
   1.240 +      path += uri.getRawFragment();
   1.241 +    }
   1.242 +
   1.243 +    int port = uri.getPort();
   1.244 +    String scheme = uri.getScheme();
   1.245 +    if (port != -1) {
   1.246 +    } else if ("http".equalsIgnoreCase(scheme)) {
   1.247 +      port = 80;
   1.248 +    } else if ("https".equalsIgnoreCase(scheme)) {
   1.249 +      port = 443;
   1.250 +    } else {
   1.251 +      throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + ".");
   1.252 +    }
   1.253 +
   1.254 +    String requestString = timestamp + "\n" +
   1.255 +        nonce       + "\n" +
   1.256 +        method      + "\n" +
   1.257 +        path        + "\n" +
   1.258 +        host        + "\n" +
   1.259 +        port        + "\n" +
   1.260 +        extra       + "\n";
   1.261 +
   1.262 +    return requestString;
   1.263 +  }
   1.264 +}

mercurial