зеркало из https://github.com/mozilla/gecko-dev.git
Bug 929066 - Handle skew in HAWK requests. r=nalexander
This commit is contained in:
Родитель
74c9eb6feb
Коммит
ac232563d5
|
@ -508,6 +508,7 @@ sync_java_files = [
|
|||
'background/fxa/FxAccountClient20.java',
|
||||
'background/fxa/FxAccountClientException.java',
|
||||
'background/fxa/FxAccountUtils.java',
|
||||
'background/fxa/SkewHandler.java',
|
||||
'background/healthreport/Environment.java',
|
||||
'background/healthreport/EnvironmentBuilder.java',
|
||||
'background/healthreport/EnvironmentV1.java',
|
||||
|
|
|
@ -150,6 +150,7 @@ public class FxAccountClient10 {
|
|||
protected final byte[] tokenId;
|
||||
protected final byte[] reqHMACKey;
|
||||
protected final boolean payload;
|
||||
protected final SkewHandler skewHandler;
|
||||
|
||||
/**
|
||||
* Create a delegate for an un-authenticated resource.
|
||||
|
@ -167,12 +168,13 @@ public class FxAccountClient10 {
|
|||
this.reqHMACKey = reqHMACKey;
|
||||
this.tokenId = tokenId;
|
||||
this.payload = authenticatePayload;
|
||||
this.skewHandler = SkewHandler.getSkewHandlerForResource(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthHeaderProvider getAuthHeaderProvider() {
|
||||
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();
|
||||
}
|
||||
|
@ -182,9 +184,14 @@ public class FxAccountClient10 {
|
|||
final int status = response.getStatusLine().getStatusCode();
|
||||
switch (status) {
|
||||
case 200:
|
||||
skewHandler.updateSkew(response, now());
|
||||
invokeHandleSuccess(status, response);
|
||||
return;
|
||||
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);
|
||||
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,
|
||||
final String srpSalt, final String mainSalt,
|
||||
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.FxAccountClient20;
|
||||
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.JSONWebTokenUtils;
|
||||
import org.mozilla.gecko.browserid.VerifyingPublicKey;
|
||||
|
@ -57,6 +58,8 @@ public class FxAccountLoginPolicy {
|
|||
return new FxAccountClient20(serverURI, executor);
|
||||
}
|
||||
|
||||
private SkewHandler skewHandler;
|
||||
|
||||
/**
|
||||
* Check if this certificate is not worth generating an assertion from: for
|
||||
* example, because it is not well-formed, or it is already expired.
|
||||
|
@ -83,6 +86,10 @@ public class FxAccountLoginPolicy {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected long now() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public enum AccountState {
|
||||
Invalid,
|
||||
NeedsSessionToken,
|
||||
|
@ -167,6 +174,11 @@ public class FxAccountLoginPolicy {
|
|||
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.
|
||||
* <p>
|
||||
|
@ -275,6 +287,10 @@ public class FxAccountLoginPolicy {
|
|||
|
||||
@Override
|
||||
public void handleFailure(int status, HttpResponse response) {
|
||||
if (skewHandler != null) {
|
||||
skewHandler.updateSkew(response, now());
|
||||
}
|
||||
|
||||
if (status != 401) {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
|
@ -320,6 +336,10 @@ public class FxAccountLoginPolicy {
|
|||
|
||||
@Override
|
||||
public void handleFailure(int status, HttpResponse response) {
|
||||
if (skewHandler != null) {
|
||||
skewHandler.updateSkew(response, now());
|
||||
}
|
||||
|
||||
if (status != 401) {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
|
@ -379,6 +399,10 @@ public class FxAccountLoginPolicy {
|
|||
|
||||
@Override
|
||||
public void handleFailure(int status, HttpResponse response) {
|
||||
if (skewHandler != null) {
|
||||
skewHandler.updateSkew(response, now());
|
||||
}
|
||||
|
||||
if (status != 401) {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
|
@ -426,6 +450,10 @@ public class FxAccountLoginPolicy {
|
|||
|
||||
@Override
|
||||
public void handleFailure(int status, HttpResponse response) {
|
||||
if (skewHandler != null) {
|
||||
skewHandler.updateSkew(response, now());
|
||||
}
|
||||
|
||||
if (status != 401) {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
|
@ -473,6 +501,10 @@ public class FxAccountLoginPolicy {
|
|||
|
||||
@Override
|
||||
public void handleFailure(int status, HttpResponse response) {
|
||||
if (skewHandler != null) {
|
||||
skewHandler.updateSkew(response, now());
|
||||
}
|
||||
|
||||
if (status != 401) {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
|
|
|
@ -11,6 +11,7 @@ import java.util.concurrent.Executors;
|
|||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
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.BrowserIDVerifierDelegate;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
|
@ -188,8 +189,18 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
|
|||
FxAccountGlobalSession globalSession = null;
|
||||
try {
|
||||
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.start();
|
||||
} catch (Exception e) {
|
||||
|
|
|
@ -69,7 +69,7 @@ public class BaseResource implements Resource {
|
|||
|
||||
private static final String LOG_TAG = "BaseResource";
|
||||
|
||||
protected URI uri;
|
||||
protected final URI uri;
|
||||
protected BasicHttpContext context;
|
||||
protected DefaultHttpClient client;
|
||||
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());
|
||||
} catch (URISyntaxException e) {
|
||||
Logger.error(LOG_TAG, "Got error rewriting URI for Android emulator.", e);
|
||||
throw new IllegalArgumentException("Invalid URI", e);
|
||||
}
|
||||
} else {
|
||||
this.uri = uri;
|
||||
|
@ -121,10 +122,21 @@ public class BaseResource implements Resource {
|
|||
httpResponseObserver = new WeakReference<HttpResponseObserver>(newHttpResponseObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() {
|
||||
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
|
||||
* 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
|
||||
* Browser-ID assertions in the format expected by a Mozilla Services Token
|
||||
* BrowserID assertions in the format expected by a Mozilla Services Token
|
||||
* Server.
|
||||
* <p>
|
||||
* 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 byte[] key;
|
||||
protected final boolean includePayloadHash;
|
||||
protected final long skewSeconds;
|
||||
|
||||
/**
|
||||
* Create a Hawk Authorization header provider.
|
||||
|
@ -63,8 +64,12 @@ public class HawkAuthHeaderProvider implements AuthHeaderProvider {
|
|||
* @param includePayloadHash
|
||||
* 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>.
|
||||
*
|
||||
* @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) {
|
||||
throw new IllegalArgumentException("id must not be null");
|
||||
}
|
||||
|
@ -74,11 +79,33 @@ public class HawkAuthHeaderProvider implements AuthHeaderProvider {
|
|||
this.id = id;
|
||||
this.key = key;
|
||||
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
|
||||
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 extra = "";
|
||||
|
||||
|
|
|
@ -4,9 +4,14 @@
|
|||
|
||||
package org.mozilla.gecko.sync.net;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpEntity;
|
||||
|
||||
public interface Resource {
|
||||
public abstract URI getURI();
|
||||
public abstract String getURIString();
|
||||
public abstract String getHostname();
|
||||
public abstract void get();
|
||||
public abstract void delete();
|
||||
public abstract void post(HttpEntity body);
|
||||
|
|
|
@ -78,6 +78,21 @@ public class SyncStorageRequest implements Resource {
|
|||
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.
|
||||
*/
|
||||
|
|
Загрузка…
Ссылка в новой задаче