Bug 929066 - Handle skew in HAWK requests. r=nalexander

This commit is contained in:
Richard Newman 2014-01-18 19:49:25 -08:00
Родитель 74c9eb6feb
Коммит ac232563d5
10 изменённых файлов: 233 добавлений и 7 удалений

Просмотреть файл

@ -508,6 +508,7 @@ sync_java_files = [
'background/fxa/FxAccountClient20.java', 'background/fxa/FxAccountClient20.java',
'background/fxa/FxAccountClientException.java', 'background/fxa/FxAccountClientException.java',
'background/fxa/FxAccountUtils.java', 'background/fxa/FxAccountUtils.java',
'background/fxa/SkewHandler.java',
'background/healthreport/Environment.java', 'background/healthreport/Environment.java',
'background/healthreport/EnvironmentBuilder.java', 'background/healthreport/EnvironmentBuilder.java',
'background/healthreport/EnvironmentV1.java', 'background/healthreport/EnvironmentV1.java',

Просмотреть файл

@ -150,6 +150,7 @@ public class FxAccountClient10 {
protected final byte[] tokenId; protected final byte[] tokenId;
protected final byte[] reqHMACKey; protected final byte[] reqHMACKey;
protected final boolean payload; protected final boolean payload;
protected final SkewHandler skewHandler;
/** /**
* Create a delegate for an un-authenticated resource. * Create a delegate for an un-authenticated resource.
@ -167,12 +168,13 @@ public class FxAccountClient10 {
this.reqHMACKey = reqHMACKey; this.reqHMACKey = reqHMACKey;
this.tokenId = tokenId; this.tokenId = tokenId;
this.payload = authenticatePayload; this.payload = authenticatePayload;
this.skewHandler = SkewHandler.getSkewHandlerForResource(resource);
} }
@Override @Override
public AuthHeaderProvider getAuthHeaderProvider() { public AuthHeaderProvider getAuthHeaderProvider() {
if (tokenId != null && reqHMACKey != null) { if (tokenId != null && reqHMACKey != null) {
return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, payload); return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, payload, skewHandler.getSkewInSeconds());
} }
return super.getAuthHeaderProvider(); return super.getAuthHeaderProvider();
} }
@ -182,9 +184,14 @@ public class FxAccountClient10 {
final int status = response.getStatusLine().getStatusCode(); final int status = response.getStatusLine().getStatusCode();
switch (status) { switch (status) {
case 200: case 200:
skewHandler.updateSkew(response, now());
invokeHandleSuccess(status, response); invokeHandleSuccess(status, response);
return; return;
default: default:
if (!skewHandler.updateSkew(response, now())) {
// If we couldn't update skew, but we got a failure, let's try clearing the skew.
skewHandler.resetSkew();
}
invokeHandleFailure(status, response); invokeHandleFailure(status, response);
return; return;
} }
@ -242,6 +249,11 @@ public class FxAccountClient10 {
} }
} }
@SuppressWarnings("static-method")
public long now() {
return System.currentTimeMillis();
}
public void createAccount(final String email, final byte[] stretchedPWBytes, public void createAccount(final String email, final byte[] stretchedPWBytes,
final String srpSalt, final String mainSalt, final String srpSalt, final String mainSalt,
final RequestDelegate<String> delegate) { final RequestDelegate<String> delegate) {

Просмотреть файл

@ -0,0 +1,111 @@
/* 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.background.fxa;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.net.Resource;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHeaders;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.impl.cookie.DateParseException;
import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
public class SkewHandler {
private static final String LOG_TAG = "SkewHandler";
protected volatile long skewMillis = 0L;
protected final String hostname;
private static final HashMap<String, SkewHandler> skewHandlers = new HashMap<String, SkewHandler>();
public static SkewHandler getSkewHandlerForResource(final Resource resource) {
return getSkewHandlerForHostname(resource.getHostname());
}
public static SkewHandler getSkewHandlerFromEndpointString(final String url) throws URISyntaxException {
if (url == null) {
throw new IllegalArgumentException("url must not be null.");
}
URI u = new URI(url);
return getSkewHandlerForHostname(u.getHost());
}
public static synchronized SkewHandler getSkewHandlerForHostname(final String hostname) {
SkewHandler handler = skewHandlers.get(hostname);
if (handler == null) {
handler = new SkewHandler(hostname);
skewHandlers.put(hostname, handler);
}
return handler;
}
public static synchronized void clearSkewHandlers() {
skewHandlers.clear();
}
public SkewHandler(final String hostname) {
this.hostname = hostname;
}
public boolean updateSkewFromServerMillis(long millis, long now) {
skewMillis = millis - now;
Logger.debug(LOG_TAG, "Updated skew: " + skewMillis + "ms for hostname " + this.hostname);
return true;
}
public boolean updateSkewFromHTTPDateString(String date, long now) {
try {
final long millis = DateUtils.parseDate(date).getTime();
return updateSkewFromServerMillis(millis, now);
} catch (DateParseException e) {
Logger.warn(LOG_TAG, "Unexpected: invalid Date header from " + this.hostname);
return false;
}
}
public boolean updateSkewFromDateHeader(Header header, long now) {
String date = header.getValue();
if (null == date) {
Logger.warn(LOG_TAG, "Unexpected: null Date header from " + this.hostname);
return false;
}
return updateSkewFromHTTPDateString(date, now);
}
/**
* Update our tracked skew value to account for the local clock differing from
* the server's.
*
* @param response
* the received HTTP response.
* @param now
* the current time in milliseconds.
* @return true if the skew value was updated, false otherwise.
*/
public boolean updateSkew(HttpResponse response, long now) {
Header header = response.getFirstHeader(HttpHeaders.DATE);
if (null == header) {
Logger.warn(LOG_TAG, "Unexpected: missing Date header from " + this.hostname);
return false;
}
return updateSkewFromDateHeader(header, now);
}
public long getSkewInMillis() {
return skewMillis;
}
public long getSkewInSeconds() {
return skewMillis / 1000;
}
public void resetSkew() {
skewMillis = 0L;
}
}

Просмотреть файл

@ -15,6 +15,7 @@ import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys; import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
import org.mozilla.gecko.background.fxa.FxAccountClient20; import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse; import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.SkewHandler;
import org.mozilla.gecko.browserid.BrowserIDKeyPair; import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils; import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.browserid.VerifyingPublicKey; import org.mozilla.gecko.browserid.VerifyingPublicKey;
@ -57,6 +58,8 @@ public class FxAccountLoginPolicy {
return new FxAccountClient20(serverURI, executor); return new FxAccountClient20(serverURI, executor);
} }
private SkewHandler skewHandler;
/** /**
* Check if this certificate is not worth generating an assertion from: for * Check if this certificate is not worth generating an assertion from: for
* example, because it is not well-formed, or it is already expired. * example, because it is not well-formed, or it is already expired.
@ -83,6 +86,10 @@ public class FxAccountLoginPolicy {
return false; return false;
} }
protected long now() {
return System.currentTimeMillis();
}
public enum AccountState { public enum AccountState {
Invalid, Invalid,
NeedsSessionToken, NeedsSessionToken,
@ -167,6 +174,11 @@ public class FxAccountLoginPolicy {
return stages; return stages;
} }
public void login(final String audience, final FxAccountLoginDelegate delegate, final SkewHandler skewHandler) {
this.skewHandler = skewHandler;
this.login(audience, delegate);
}
/** /**
* Do as much of a Firefox Account login dance as possible. * Do as much of a Firefox Account login dance as possible.
* <p> * <p>
@ -275,6 +287,10 @@ public class FxAccountLoginPolicy {
@Override @Override
public void handleFailure(int status, HttpResponse response) { public void handleFailure(int status, HttpResponse response) {
if (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
if (status != 401) { if (status != 401) {
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
return; return;
@ -320,6 +336,10 @@ public class FxAccountLoginPolicy {
@Override @Override
public void handleFailure(int status, HttpResponse response) { public void handleFailure(int status, HttpResponse response) {
if (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
if (status != 401) { if (status != 401) {
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
return; return;
@ -379,6 +399,10 @@ public class FxAccountLoginPolicy {
@Override @Override
public void handleFailure(int status, HttpResponse response) { public void handleFailure(int status, HttpResponse response) {
if (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
if (status != 401) { if (status != 401) {
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
return; return;
@ -426,6 +450,10 @@ public class FxAccountLoginPolicy {
@Override @Override
public void handleFailure(int status, HttpResponse response) { public void handleFailure(int status, HttpResponse response) {
if (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
if (status != 401) { if (status != 401) {
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
return; return;
@ -473,6 +501,10 @@ public class FxAccountLoginPolicy {
@Override @Override
public void handleFailure(int status, HttpResponse response) { public void handleFailure(int status, HttpResponse response) {
if (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
if (status != 401) { if (status != 401) {
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response)))); delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
return; return;

Просмотреть файл

@ -11,6 +11,7 @@ import java.util.concurrent.Executors;
import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils; import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.SkewHandler;
import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient; import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient;
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate; import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate;
import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.FxAccountConstants;
@ -188,8 +189,18 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
FxAccountGlobalSession globalSession = null; FxAccountGlobalSession globalSession = null;
try { try {
ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs); ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
final KeyBundle syncKeyBundle = FxAccountUtils.generateSyncKeyBundle(fxAccount.getKb()); // TODO Document this choice for deriving from kB.
AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false); // TODO Document this choice for deriving from kB.
final KeyBundle syncKeyBundle = FxAccountUtils.generateSyncKeyBundle(fxAccount.getKb());
// We compute skew over time using SkewHandler. This yields an unchanging
// skew adjustment that the HawkAuthHeaderProvider uses to adjust its
// timestamps. Eventually we might want this to adapt within the scope of a
// global session.
final SkewHandler tokenServerSkewHandler = SkewHandler.getSkewHandlerFromEndpointString(token.endpoint);
final long tokenServerSkew = tokenServerSkewHandler.getSkewInSeconds();
AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false, tokenServerSkew);
globalSession = new FxAccountGlobalSession(token.endpoint, token.uid, authHeaderProvider, FxAccountConstants.PREFS_PATH, syncKeyBundle, callback, getContext(), extras, clientsDataDelegate); globalSession = new FxAccountGlobalSession(token.endpoint, token.uid, authHeaderProvider, FxAccountConstants.PREFS_PATH, syncKeyBundle, callback, getContext(), extras, clientsDataDelegate);
globalSession.start(); globalSession.start();
} catch (Exception e) { } catch (Exception e) {

Просмотреть файл

@ -69,7 +69,7 @@ public class BaseResource implements Resource {
private static final String LOG_TAG = "BaseResource"; private static final String LOG_TAG = "BaseResource";
protected URI uri; protected final URI uri;
protected BasicHttpContext context; protected BasicHttpContext context;
protected DefaultHttpClient client; protected DefaultHttpClient client;
public ResourceDelegate delegate; public ResourceDelegate delegate;
@ -101,6 +101,7 @@ public class BaseResource implements Resource {
this.uri = new URI(uri.getScheme(), uri.getUserInfo(), ANDROID_LOOPBACK_IP, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); this.uri = new URI(uri.getScheme(), uri.getUserInfo(), ANDROID_LOOPBACK_IP, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
Logger.error(LOG_TAG, "Got error rewriting URI for Android emulator.", e); Logger.error(LOG_TAG, "Got error rewriting URI for Android emulator.", e);
throw new IllegalArgumentException("Invalid URI", e);
} }
} else { } else {
this.uri = uri; this.uri = uri;
@ -121,10 +122,21 @@ public class BaseResource implements Resource {
httpResponseObserver = new WeakReference<HttpResponseObserver>(newHttpResponseObserver); httpResponseObserver = new WeakReference<HttpResponseObserver>(newHttpResponseObserver);
} }
@Override
public URI getURI() { public URI getURI() {
return this.uri; return this.uri;
} }
@Override
public String getURIString() {
return this.uri.toString();
}
@Override
public String getHostname() {
return this.getURI().getHost();
}
/** /**
* This shuts up HttpClient, which will otherwise debug log about there * This shuts up HttpClient, which will otherwise debug log about there
* being no auth cache in the context. * being no auth cache in the context.

Просмотреть файл

@ -12,7 +12,7 @@ import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
/** /**
* An <code>AuthHeaderProvider</code> that returns an Authorization header for * An <code>AuthHeaderProvider</code> that returns an Authorization header for
* Browser-ID assertions in the format expected by a Mozilla Services Token * BrowserID assertions in the format expected by a Mozilla Services Token
* Server. * Server.
* <p> * <p>
* See <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>. * See <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>.

Просмотреть файл

@ -48,6 +48,7 @@ public class HawkAuthHeaderProvider implements AuthHeaderProvider {
protected final String id; protected final String id;
protected final byte[] key; protected final byte[] key;
protected final boolean includePayloadHash; protected final boolean includePayloadHash;
protected final long skewSeconds;
/** /**
* Create a Hawk Authorization header provider. * Create a Hawk Authorization header provider.
@ -63,8 +64,12 @@ public class HawkAuthHeaderProvider implements AuthHeaderProvider {
* @param includePayloadHash * @param includePayloadHash
* true if message integrity hash should be included in signed * true if message integrity hash should be included in signed
* request header. See <a href="https://github.com/hueniverse/hawk#payload-validation">https://github.com/hueniverse/hawk#payload-validation</a>. * request header. See <a href="https://github.com/hueniverse/hawk#payload-validation">https://github.com/hueniverse/hawk#payload-validation</a>.
*
* @param skewSeconds
* a number of seconds by which to skew the current time when
* computing a header.
*/ */
public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash) { public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash, long skewSeconds) {
if (id == null) { if (id == null) {
throw new IllegalArgumentException("id must not be null"); throw new IllegalArgumentException("id must not be null");
} }
@ -74,11 +79,33 @@ public class HawkAuthHeaderProvider implements AuthHeaderProvider {
this.id = id; this.id = id;
this.key = key; this.key = key;
this.includePayloadHash = includePayloadHash; this.includePayloadHash = includePayloadHash;
this.skewSeconds = skewSeconds;
}
public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash) {
this(id, key, includePayloadHash, 0L);
}
/**
* @return the current time in milliseconds.
*/
@SuppressWarnings("static-method")
protected long now() {
return System.currentTimeMillis();
}
/**
* @return the current time in seconds, adjusted for skew. This should
* approximate the server's timestamp.
*/
protected long getTimestampSeconds() {
return (now() / 1000) + skewSeconds;
} }
@Override @Override
public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException { public Header getAuthHeader(HttpRequestBase request, BasicHttpContext context, DefaultHttpClient client) throws GeneralSecurityException {
long timestamp = System.currentTimeMillis() / 1000; long timestamp = getTimestampSeconds();
String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES)); String nonce = Base64.encodeBase64String(Utils.generateRandomBytes(NONCE_LENGTH_IN_BYTES));
String extra = ""; String extra = "";

Просмотреть файл

@ -4,9 +4,14 @@
package org.mozilla.gecko.sync.net; package org.mozilla.gecko.sync.net;
import java.net.URI;
import ch.boye.httpclientandroidlib.HttpEntity; import ch.boye.httpclientandroidlib.HttpEntity;
public interface Resource { public interface Resource {
public abstract URI getURI();
public abstract String getURIString();
public abstract String getHostname();
public abstract void get(); public abstract void get();
public abstract void delete(); public abstract void delete();
public abstract void post(HttpEntity body); public abstract void post(HttpEntity body);

Просмотреть файл

@ -78,6 +78,21 @@ public class SyncStorageRequest implements Resource {
this.resource.delegate = this.resourceDelegate; this.resource.delegate = this.resourceDelegate;
} }
@Override
public URI getURI() {
return this.resource.getURI();
}
@Override
public String getURIString() {
return this.resource.getURIString();
}
@Override
public String getHostname() {
return this.resource.getHostname();
}
/** /**
* A ResourceDelegate that mediates between Resource-level notifications and the SyncStorageRequest. * A ResourceDelegate that mediates between Resource-level notifications and the SyncStorageRequest.
*/ */