Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.sync.net;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.io.UnsupportedEncodingException;
10 import java.net.URI;
11 import java.security.GeneralSecurityException;
12 import java.security.InvalidKeyException;
13 import java.security.MessageDigest;
14 import java.security.NoSuchAlgorithmException;
15 import java.util.Locale;
17 import javax.crypto.Mac;
18 import javax.crypto.spec.SecretKeySpec;
20 import org.mozilla.apache.commons.codec.binary.Base64;
21 import org.mozilla.gecko.background.common.log.Logger;
22 import org.mozilla.gecko.sync.Utils;
24 import ch.boye.httpclientandroidlib.Header;
25 import ch.boye.httpclientandroidlib.HttpEntity;
26 import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
27 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
28 import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
29 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
30 import ch.boye.httpclientandroidlib.message.BasicHeader;
31 import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
33 /**
34 * An <code>AuthHeaderProvider</code> that returns an Authorization header for
35 * Hawk: <a href="https://github.com/hueniverse/hawk">https://github.com/hueniverse/hawk</a>.
36 *
37 * Hawk is an HTTP authentication scheme using a message authentication code
38 * (MAC) algorithm to provide partial HTTP request cryptographic verification.
39 * Hawk is the successor to the HMAC authentication scheme.
40 */
41 public class HawkAuthHeaderProvider implements AuthHeaderProvider {
42 public static final String LOG_TAG = HawkAuthHeaderProvider.class.getSimpleName();
44 public static final int HAWK_HEADER_VERSION = 1;
46 protected static final int NONCE_LENGTH_IN_BYTES = 8;
47 protected static final String HMAC_SHA256_ALGORITHM = "hmacSHA256";
49 protected final String id;
50 protected final byte[] key;
51 protected final boolean includePayloadHash;
52 protected final long skewSeconds;
54 /**
55 * Create a Hawk Authorization header provider.
56 * <p>
57 * Hawk specifies no mechanism by which a client receives an
58 * identifier-and-key pair from the server.
59 * <p>
60 * Hawk requests can include a payload verification hash with requests that
61 * enclose an entity (PATCH, POST, and PUT requests). <b>You should default
62 * to including the payload verification hash<b> unless you have a good reason
63 * not to -- the server can always ignore payload verification hashes provided
64 * by the client.
65 *
66 * @param id
67 * to name requests with.
68 * @param key
69 * to sign request with.
70 *
71 * @param includePayloadHash
72 * true if payload verification hash should be included in signed
73 * request header. See <a href="https://github.com/hueniverse/hawk#payload-validation">https://github.com/hueniverse/hawk#payload-validation</a>.
74 *
75 * @param skewSeconds
76 * a number of seconds by which to skew the current time when
77 * computing a header.
78 */
79 public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash, long skewSeconds) {
80 if (id == null) {
81 throw new IllegalArgumentException("id must not be null");
82 }
83 if (key == null) {
84 throw new IllegalArgumentException("key must not be null");
85 }
86 this.id = id;
87 this.key = key;
88 this.includePayloadHash = includePayloadHash;
89 this.skewSeconds = skewSeconds;
90 }
92 /**
93 * @return the current time in milliseconds.
94 */
95 @SuppressWarnings("static-method")
96 protected long now() {
97 return System.currentTimeMillis();
98 }
100 /**
101 * @return the current time in seconds, adjusted for skew. This should
102 * approximate the server's timestamp.
103 */
104 protected long getTimestampSeconds() {
105 return (now() / 1000) + skewSeconds;
106 }
108 @Override
109 public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
110 long timestamp = getTimestampSeconds();
111 String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES));
112 String extra = "";
114 try {
115 return getAuthHeader(request, context, client, timestamp, nonce, extra, this.includePayloadHash);
116 } catch (Exception e) {
117 // We lie a little and make every exception a GeneralSecurityException.
118 throw new GeneralSecurityException(e);
119 }
120 }
122 /**
123 * Helper function that generates an HTTP Authorization: Hawk header given
124 * additional Hawk specific data.
125 *
126 * @throws NoSuchAlgorithmException
127 * @throws InvalidKeyException
128 * @throws IOException
129 */
130 protected Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client,
131 long timestamp, String nonce, String extra, boolean includePayloadHash)
132 throws InvalidKeyException, NoSuchAlgorithmException, IOException {
133 if (timestamp < 0) {
134 throw new IllegalArgumentException("timestamp must contain only [0-9].");
135 }
137 if (nonce == null) {
138 throw new IllegalArgumentException("nonce must not be null.");
139 }
140 if (nonce.length() == 0) {
141 throw new IllegalArgumentException("nonce must not be empty.");
142 }
144 String payloadHash = null;
145 if (includePayloadHash) {
146 payloadHash = getPayloadHashString(request);
147 } else {
148 Logger.debug(LOG_TAG, "Configured to not include payload hash for this request.");
149 }
151 String app = null;
152 String dlg = null;
153 String requestString = getRequestString(request, "header", timestamp, nonce, payloadHash, extra, app, dlg);
154 String macString = getSignature(requestString.getBytes("UTF-8"), this.key);
156 StringBuilder sb = new StringBuilder();
157 sb.append("Hawk id=\"");
158 sb.append(this.id);
159 sb.append("\", ");
160 sb.append("ts=\"");
161 sb.append(timestamp);
162 sb.append("\", ");
163 sb.append("nonce=\"");
164 sb.append(nonce);
165 sb.append("\", ");
166 if (payloadHash != null) {
167 sb.append("hash=\"");
168 sb.append(payloadHash);
169 sb.append("\", ");
170 }
171 if (extra != null && extra.length() > 0) {
172 sb.append("ext=\"");
173 sb.append(escapeExtraHeaderAttribute(extra));
174 sb.append("\", ");
175 }
176 sb.append("mac=\"");
177 sb.append(macString);
178 sb.append("\"");
180 return new BasicHeader("Authorization", sb.toString());
181 }
183 /**
184 * Get the payload verification hash for the given request, if possible.
185 * <p>
186 * Returns null if the request does not enclose an entity (is not an HTTP
187 * PATCH, POST, or PUT). Throws if the payload verification hash cannot be
188 * computed.
189 *
190 * @param request
191 * to compute hash for.
192 * @return verification hash, or null if the request does not enclose an entity.
193 * @throws IllegalArgumentException if the request does not enclose a valid non-null entity.
194 * @throws UnsupportedEncodingException
195 * @throws NoSuchAlgorithmException
196 * @throws IOException
197 */
198 protected static String getPayloadHashString(HttpRequestBase request)
199 throws UnsupportedEncodingException, NoSuchAlgorithmException, IOException, IllegalArgumentException {
200 final boolean shouldComputePayloadHash = request instanceof HttpEntityEnclosingRequest;
201 if (!shouldComputePayloadHash) {
202 Logger.debug(LOG_TAG, "Not computing payload verification hash for non-enclosing request.");
203 return null;
204 }
205 if (!(request instanceof HttpEntityEnclosingRequest)) {
206 throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request without an entity");
207 }
208 final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
209 if (entity == null) {
210 throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request with a null entity");
211 }
212 return Base64.encodeBase64String(getPayloadHash(entity));
213 }
215 /**
216 * Escape the user-provided extra string for the ext="" header attribute.
217 * <p>
218 * Hawk escapes the header ext="" attribute differently than it does the extra
219 * line in the normalized request string.
220 * <p>
221 * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385</a>.
222 *
223 * @param extra to escape.
224 * @return extra escaped for the ext="" header attribute.
225 */
226 protected static String escapeExtraHeaderAttribute(String extra) {
227 return extra.replaceAll("\\\\", "\\\\").replaceAll("\"", "\\\"");
228 }
230 /**
231 * Escape the user-provided extra string for inserting into the normalized
232 * request string.
233 * <p>
234 * Hawk escapes the header ext="" attribute differently than it does the extra
235 * line in the normalized request string.
236 * <p>
237 * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L67</a>.
238 *
239 * @param extra to escape.
240 * @return extra escaped for the normalized request string.
241 */
242 protected static String escapeExtraString(String extra) {
243 return extra.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\n");
244 }
246 /**
247 * Return the content type with no parameters (pieces following ;).
248 *
249 * @param contentTypeHeader to interrogate.
250 * @return base content type.
251 */
252 protected static String getBaseContentType(Header contentTypeHeader) {
253 if (contentTypeHeader == null) {
254 throw new IllegalArgumentException("contentTypeHeader must not be null.");
255 }
256 String contentType = contentTypeHeader.getValue();
257 if (contentType == null) {
258 throw new IllegalArgumentException("contentTypeHeader value must not be null.");
259 }
260 int index = contentType.indexOf(";");
261 if (index < 0) {
262 return contentType.trim();
263 }
264 return contentType.substring(0, index).trim();
265 }
267 /**
268 * Generate the SHA-256 hash of a normalized Hawk payload generated from an
269 * HTTP entity.
270 * <p>
271 * <b>Warning:</b> the entity <b>must</b> be repeatable. If it is not, this
272 * code throws an <code>IllegalArgumentException</code>.
273 * <p>
274 * This is under-specified; the code here was reverse engineered from the code
275 * at
276 * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L81</a>.
277 * @param entity to normalize and hash.
278 * @return hash.
279 * @throws IllegalArgumentException if entity is not repeatable.
280 */
281 protected static byte[] getPayloadHash(HttpEntity entity) throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException {
282 if (!entity.isRepeatable()) {
283 throw new IllegalArgumentException("entity must be repeatable");
284 }
285 final MessageDigest digest = MessageDigest.getInstance("SHA-256");
286 digest.update(("hawk." + HAWK_HEADER_VERSION + ".payload\n").getBytes("UTF-8"));
287 digest.update(getBaseContentType(entity.getContentType()).getBytes("UTF-8"));
288 digest.update("\n".getBytes("UTF-8"));
289 InputStream stream = entity.getContent();
290 try {
291 int numRead;
292 byte[] buffer = new byte[4096];
293 while (-1 != (numRead = stream.read(buffer))) {
294 if (numRead > 0) {
295 digest.update(buffer, 0, numRead);
296 }
297 }
298 digest.update("\n".getBytes("UTF-8")); // Trailing newline is specified by Hawk.
299 return digest.digest();
300 } finally {
301 stream.close();
302 }
303 }
305 /**
306 * Generate a normalized Hawk request string. This is under-specified; the
307 * code here was reverse engineered from the code at
308 * <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/crypto.js#L55</a>.
309 * <p>
310 * This method trusts its inputs to be valid.
311 */
312 protected static String getRequestString(HttpUriRequest request, String type, long timestamp, String nonce, String hash, String extra, String app, String dlg) {
313 String method = request.getMethod().toUpperCase(Locale.US);
315 URI uri = request.getURI();
316 String host = uri.getHost();
318 String path = uri.getRawPath();
319 if (uri.getRawQuery() != null) {
320 path += "?";
321 path += uri.getRawQuery();
322 }
323 if (uri.getRawFragment() != null) {
324 path += "#";
325 path += uri.getRawFragment();
326 }
328 int port = uri.getPort();
329 String scheme = uri.getScheme();
330 if (port != -1) {
331 } else if ("http".equalsIgnoreCase(scheme)) {
332 port = 80;
333 } else if ("https".equalsIgnoreCase(scheme)) {
334 port = 443;
335 } else {
336 throw new IllegalArgumentException("Unsupported URI scheme: " + scheme + ".");
337 }
339 StringBuilder sb = new StringBuilder();
340 sb.append("hawk.");
341 sb.append(HAWK_HEADER_VERSION);
342 sb.append('.');
343 sb.append(type);
344 sb.append('\n');
345 sb.append(timestamp);
346 sb.append('\n');
347 sb.append(nonce);
348 sb.append('\n');
349 sb.append(method);
350 sb.append('\n');
351 sb.append(path);
352 sb.append('\n');
353 sb.append(host);
354 sb.append('\n');
355 sb.append(port);
356 sb.append('\n');
357 if (hash != null) {
358 sb.append(hash);
359 }
360 sb.append("\n");
361 if (extra != null && extra.length() > 0) {
362 sb.append(escapeExtraString(extra));
363 }
364 sb.append("\n");
365 if (app != null) {
366 sb.append(app);
367 sb.append("\n");
368 if (dlg != null) {
369 sb.append(dlg);
370 }
371 sb.append("\n");
372 }
374 return sb.toString();
375 }
377 protected static byte[] hmacSha256(byte[] message, byte[] key)
378 throws NoSuchAlgorithmException, InvalidKeyException {
380 SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_SHA256_ALGORITHM);
382 Mac hasher = Mac.getInstance(HMAC_SHA256_ALGORITHM);
383 hasher.init(keySpec);
384 hasher.update(message);
386 return hasher.doFinal();
387 }
389 /**
390 * Sign a Hawk request string.
391 *
392 * @param requestString to sign.
393 * @param key as <code>String</code>.
394 * @return signature as base-64 encoded string.
395 * @throws InvalidKeyException
396 * @throws NoSuchAlgorithmException
397 * @throws UnsupportedEncodingException
398 */
399 protected static String getSignature(byte[] requestString, byte[] key)
400 throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
401 return Base64.encodeBase64String(hmacSha256(requestString, key));
402 }
403 }