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