mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-12 17:22:04 +00:00
Bug 956844 - Part 2: rework FxA state machine. r=rnewman
This commit is contained in:
parent
1948178e6a
commit
a60fb79516
mobile/android/base
@ -503,6 +503,7 @@ sync_java_files = [
|
||||
'background/fxa/FxAccount10CreateDelegate.java',
|
||||
'background/fxa/FxAccount20CreateDelegate.java',
|
||||
'background/fxa/FxAccount20LoginDelegate.java',
|
||||
'background/fxa/FxAccountClient.java',
|
||||
'background/fxa/FxAccountClient10.java',
|
||||
'background/fxa/FxAccountClient20.java',
|
||||
'background/fxa/FxAccountClientException.java',
|
||||
|
@ -4,639 +4,15 @@
|
||||
|
||||
package org.mozilla.gecko.background.fxa;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
import org.mozilla.gecko.sync.crypto.HKDF;
|
||||
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
|
||||
import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
|
||||
import org.mozilla.gecko.sync.net.Resource;
|
||||
import org.mozilla.gecko.sync.net.SyncResponse;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpEntity;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
|
||||
|
||||
/**
|
||||
* An HTTP client for talking to an FxAccount server.
|
||||
* <p>
|
||||
* The reference server is developed at
|
||||
* <a href="https://github.com/mozilla/picl-idp">https://github.com/mozilla/picl-idp</a>.
|
||||
* This implementation was developed against
|
||||
* <a href="https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208">https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208</a>.
|
||||
* <p>
|
||||
* The delegate structure used is a little different from the rest of the code
|
||||
* base. We add a <code>RequestDelegate</code> layer that processes a typed
|
||||
* value extracted from the body of a successful response.
|
||||
* <p>
|
||||
* Further, we add internal <code>CreateDelegate</code> and
|
||||
* <code>AuthDelegate</code> delegates to make it easier to modify the request
|
||||
* bodies sent to the /create and /auth endpoints.
|
||||
*/
|
||||
public class FxAccountClient {
|
||||
protected static final String LOG_TAG = FxAccountClient.class.getSimpleName();
|
||||
|
||||
protected static final String VERSION_FRAGMENT = "v1/";
|
||||
|
||||
public static final String JSON_KEY_EMAIL = "email";
|
||||
public static final String JSON_KEY_KEYFETCHTOKEN = "keyFetchToken";
|
||||
public static final String JSON_KEY_SESSIONTOKEN = "sessionToken";
|
||||
public static final String JSON_KEY_UID = "uid";
|
||||
public static final String JSON_KEY_VERIFIED = "verified";
|
||||
|
||||
protected final String serverURI;
|
||||
protected final Executor executor;
|
||||
|
||||
public FxAccountClient(String serverURI, Executor executor) {
|
||||
if (serverURI == null) {
|
||||
throw new IllegalArgumentException("Must provide a server URI.");
|
||||
}
|
||||
if (executor == null) {
|
||||
throw new IllegalArgumentException("Must provide a non-null executor.");
|
||||
}
|
||||
this.serverURI = (serverURI.endsWith("/") ? serverURI : serverURI + "/") + VERSION_FRAGMENT;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a typed value extracted from a successful response (in an
|
||||
* endpoint-dependent way).
|
||||
*/
|
||||
public interface RequestDelegate<T> {
|
||||
public void handleError(Exception e);
|
||||
public void handleFailure(int status, HttpResponse response);
|
||||
public void handleSuccess(T result);
|
||||
}
|
||||
|
||||
/**
|
||||
* A <code>CreateDelegate</code> produces the body of a /create request.
|
||||
*/
|
||||
public interface CreateDelegate {
|
||||
public JSONObject getCreateBody() throws FxAccountClientException;
|
||||
}
|
||||
|
||||
/**
|
||||
* A <code>AuthDelegate</code> produces the bodies of an /auth/{start,finish}
|
||||
* request pair and exposes state generated by a successful response.
|
||||
*/
|
||||
public interface AuthDelegate {
|
||||
public JSONObject getAuthStartBody() throws FxAccountClientException;
|
||||
public void onAuthStartResponse(ExtendedJSONObject body) throws FxAccountClientException;
|
||||
public JSONObject getAuthFinishBody() throws FxAccountClientException;
|
||||
|
||||
public byte[] getSharedBytes() throws FxAccountClientException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin container for two access tokens.
|
||||
*/
|
||||
public static class TwoTokens {
|
||||
public final byte[] keyFetchToken;
|
||||
public final byte[] sessionToken;
|
||||
public TwoTokens(byte[] keyFetchToken, byte[] sessionToken) {
|
||||
this.keyFetchToken = keyFetchToken;
|
||||
this.sessionToken = sessionToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin container for two cryptographic keys.
|
||||
*/
|
||||
public static class TwoKeys {
|
||||
public final byte[] kA;
|
||||
public final byte[] wrapkB;
|
||||
public TwoKeys(byte[] kA, byte[] wrapkB) {
|
||||
this.kA = kA;
|
||||
this.wrapkB = wrapkB;
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate resource callbacks into request callbacks invoked on the provided
|
||||
* executor.
|
||||
* <p>
|
||||
* Override <code>handleSuccess</code> to parse the body of the resource
|
||||
* request and call the request callback. <code>handleSuccess</code> is
|
||||
* invoked via the executor, so you don't need to delegate further.
|
||||
*/
|
||||
protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
|
||||
protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
|
||||
|
||||
protected final RequestDelegate<T> delegate;
|
||||
|
||||
protected final byte[] tokenId;
|
||||
protected final byte[] reqHMACKey;
|
||||
protected final boolean payload;
|
||||
|
||||
/**
|
||||
* Create a delegate for an un-authenticated resource.
|
||||
*/
|
||||
public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) {
|
||||
this(resource, delegate, null, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a delegate for a Hawk-authenticated resource.
|
||||
*/
|
||||
public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, final byte[] tokenId, final byte[] reqHMACKey, final boolean authenticatePayload) {
|
||||
super(resource);
|
||||
this.delegate = delegate;
|
||||
this.reqHMACKey = reqHMACKey;
|
||||
this.tokenId = tokenId;
|
||||
this.payload = authenticatePayload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthHeaderProvider getAuthHeaderProvider() {
|
||||
if (tokenId != null && reqHMACKey != null) {
|
||||
return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, payload);
|
||||
}
|
||||
return super.getAuthHeaderProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpResponse(HttpResponse response) {
|
||||
final int status = response.getStatusLine().getStatusCode();
|
||||
switch (status) {
|
||||
case 200:
|
||||
invokeHandleSuccess(status, response);
|
||||
return;
|
||||
default:
|
||||
invokeHandleFailure(status, response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected void invokeHandleFailure(final int status, final HttpResponse response) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleFailure(status, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
|
||||
ResourceDelegate.this.handleSuccess(status, response, body);
|
||||
} catch (Exception e) {
|
||||
delegate.handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpProtocolException(final ClientProtocolException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpIOException(IOException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportException(GeneralSecurityException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> void post(BaseResource resource, final JSONObject requestBody, final RequestDelegate<T> delegate) {
|
||||
try {
|
||||
if (requestBody == null) {
|
||||
resource.post((HttpEntity) null);
|
||||
} else {
|
||||
resource.post(requestBody);
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void createAccount(final String email, final byte[] stretchedPWBytes,
|
||||
final String srpSalt, final String mainSalt,
|
||||
final RequestDelegate<String> delegate) {
|
||||
try {
|
||||
createAccount(new FxAccount10CreateDelegate(email, stretchedPWBytes, srpSalt, mainSalt), delegate);
|
||||
} catch (final Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected void createAccount(final CreateDelegate createDelegate, final RequestDelegate<String> delegate) {
|
||||
JSONObject body = null;
|
||||
try {
|
||||
body = createDelegate.getCreateBody();
|
||||
} catch (FxAccountClientException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "account/create"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<String>(resource, delegate) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
String uid = body.getString("uid");
|
||||
if (uid == null) {
|
||||
delegate.handleError(new FxAccountClientException("uid must be a non-null string"));
|
||||
return;
|
||||
}
|
||||
delegate.handleSuccess(uid);
|
||||
}
|
||||
};
|
||||
post(resource, body, delegate);
|
||||
}
|
||||
|
||||
protected void authStart(final AuthDelegate authDelegate, final RequestDelegate<AuthDelegate> delegate) {
|
||||
JSONObject body;
|
||||
try {
|
||||
body = authDelegate.getAuthStartBody();
|
||||
} catch (FxAccountClientException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "auth/start"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<AuthDelegate>(resource, delegate) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
try {
|
||||
authDelegate.onAuthStartResponse(body);
|
||||
delegate.handleSuccess(authDelegate);
|
||||
} catch (Exception e) {
|
||||
delegate.handleError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
post(resource, body, delegate);
|
||||
}
|
||||
|
||||
protected void authFinish(final AuthDelegate authDelegate, RequestDelegate<byte[]> delegate) {
|
||||
JSONObject body;
|
||||
try {
|
||||
body = authDelegate.getAuthFinishBody();
|
||||
} catch (FxAccountClientException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "auth/finish"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<byte[]>(resource, delegate) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
try {
|
||||
byte[] authToken = new byte[32];
|
||||
unbundleBody(body, authDelegate.getSharedBytes(), FxAccountUtils.KW("auth/finish"), authToken);
|
||||
delegate.handleSuccess(authToken);
|
||||
} catch (Exception e) {
|
||||
delegate.handleError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
post(resource, body, delegate);
|
||||
}
|
||||
|
||||
public void login(final String email, final byte[] stretchedPWBytes, final RequestDelegate<byte[]> delegate) {
|
||||
login(new FxAccount10AuthDelegate(email, stretchedPWBytes), delegate);
|
||||
}
|
||||
|
||||
protected void login(final AuthDelegate authDelegate, final RequestDelegate<byte[]> delegate) {
|
||||
authStart(authDelegate, new RequestDelegate<AuthDelegate>() {
|
||||
@Override
|
||||
public void handleSuccess(AuthDelegate srpSession) {
|
||||
authFinish(srpSession, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(final Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFailure(final int status, final HttpResponse response) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleFailure(status, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void sessionCreate(byte[] authToken, final RequestDelegate<TwoTokens> delegate) {
|
||||
final byte[] tokenId = new byte[32];
|
||||
final byte[] reqHMACKey = new byte[32];
|
||||
final byte[] requestKey = new byte[32];
|
||||
try {
|
||||
HKDF.deriveMany(authToken, new byte[0], FxAccountUtils.KW("authToken"), tokenId, reqHMACKey, requestKey);
|
||||
} catch (Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "session/create"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<TwoTokens>(resource, delegate, tokenId, reqHMACKey, false) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
try {
|
||||
byte[] keyFetchToken = new byte[32];
|
||||
byte[] sessionToken = new byte[32];
|
||||
unbundleBody(body, requestKey, FxAccountUtils.KW("session/create"), keyFetchToken, sessionToken);
|
||||
delegate.handleSuccess(new TwoTokens(keyFetchToken, sessionToken));
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
delegate.handleError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
post(resource, null, delegate);
|
||||
}
|
||||
|
||||
public void sessionDestroy(byte[] sessionToken, final RequestDelegate<Void> delegate) {
|
||||
final byte[] tokenId = new byte[32];
|
||||
final byte[] reqHMACKey = new byte[32];
|
||||
try {
|
||||
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
|
||||
} catch (Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "session/destroy"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey, false) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
delegate.handleSuccess(null);
|
||||
}
|
||||
};
|
||||
post(resource, null, delegate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't call this directly. Use <code>unbundleBody</code> instead.
|
||||
*/
|
||||
protected void unbundleBytes(byte[] bundleBytes, byte[] respHMACKey, byte[] respXORKey, byte[]... rest)
|
||||
throws InvalidKeyException, NoSuchAlgorithmException, FxAccountClientException {
|
||||
if (bundleBytes.length < 32) {
|
||||
throw new IllegalArgumentException("input bundle must include HMAC");
|
||||
}
|
||||
int len = respXORKey.length;
|
||||
if (bundleBytes.length != len + 32) {
|
||||
throw new IllegalArgumentException("input bundle and XOR key with HMAC have different lengths");
|
||||
}
|
||||
int left = len;
|
||||
for (byte[] array : rest) {
|
||||
left -= array.length;
|
||||
}
|
||||
if (left != 0) {
|
||||
throw new IllegalArgumentException("XOR key and total output arrays have different lengths");
|
||||
}
|
||||
|
||||
byte[] ciphertext = new byte[len];
|
||||
byte[] HMAC = new byte[32];
|
||||
System.arraycopy(bundleBytes, 0, ciphertext, 0, len);
|
||||
System.arraycopy(bundleBytes, len, HMAC, 0, 32);
|
||||
|
||||
Mac hmacHasher = HKDF.makeHMACHasher(respHMACKey);
|
||||
byte[] computedHMAC = hmacHasher.doFinal(ciphertext);
|
||||
if (!Arrays.equals(computedHMAC, HMAC)) {
|
||||
throw new FxAccountClientException("Bad message HMAC");
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
for (byte[] array : rest) {
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
array[i] = (byte) (respXORKey[offset + i] ^ ciphertext[offset + i]);
|
||||
}
|
||||
offset += array.length;
|
||||
}
|
||||
}
|
||||
|
||||
protected void unbundleBody(ExtendedJSONObject body, byte[] requestKey, byte[] ctxInfo, byte[]... rest) throws Exception {
|
||||
int length = 0;
|
||||
for (byte[] array : rest) {
|
||||
length += array.length;
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
throw new FxAccountClientException("body must be non-null");
|
||||
}
|
||||
String bundle = body.getString("bundle");
|
||||
if (bundle == null) {
|
||||
throw new FxAccountClientException("bundle must be a non-null string");
|
||||
}
|
||||
byte[] bundleBytes = Utils.hex2Byte(bundle);
|
||||
|
||||
final byte[] respHMACKey = new byte[32];
|
||||
final byte[] respXORKey = new byte[length];
|
||||
HKDF.deriveMany(requestKey, new byte[0], ctxInfo, respHMACKey, respXORKey);
|
||||
unbundleBytes(bundleBytes, respHMACKey, respXORKey, rest);
|
||||
}
|
||||
|
||||
public void keys(byte[] keyFetchToken, final RequestDelegate<TwoKeys> delegate) {
|
||||
final byte[] tokenId = new byte[32];
|
||||
final byte[] reqHMACKey = new byte[32];
|
||||
final byte[] requestKey = new byte[32];
|
||||
try {
|
||||
HKDF.deriveMany(keyFetchToken, new byte[0], FxAccountUtils.KW("keyFetchToken"), tokenId, reqHMACKey, requestKey);
|
||||
} catch (Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "account/keys"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, tokenId, reqHMACKey, false) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
try {
|
||||
byte[] kA = new byte[32];
|
||||
byte[] wrapkB = new byte[32];
|
||||
unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB);
|
||||
delegate.handleSuccess(new TwoKeys(kA, wrapkB));
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
delegate.handleError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
resource.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin container for status response.
|
||||
*/
|
||||
public static class StatusResponse {
|
||||
public final String email;
|
||||
public final boolean verified;
|
||||
public StatusResponse(String email, boolean verified) {
|
||||
this.email = email;
|
||||
this.verified = verified;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the status of an account given a valid session token.
|
||||
* <p>
|
||||
* This API is a little odd: the auth server returns the email and
|
||||
* verification state of the account that corresponds to the (opaque) session
|
||||
* token. It might fail if the session token is unknown (or invalid, or
|
||||
* revoked).
|
||||
*
|
||||
* @param sessionToken
|
||||
* to query.
|
||||
* @param delegate
|
||||
* to invoke callbacks.
|
||||
*/
|
||||
public void status(byte[] sessionToken, final RequestDelegate<StatusResponse> delegate) {
|
||||
final byte[] tokenId = new byte[32];
|
||||
final byte[] reqHMACKey = new byte[32];
|
||||
final byte[] requestKey = new byte[32];
|
||||
try {
|
||||
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
|
||||
} catch (Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "recovery_email/status"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<StatusResponse>(resource, delegate, tokenId, reqHMACKey, false) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
try {
|
||||
String[] requiredStringFields = new String[] { JSON_KEY_EMAIL };
|
||||
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
|
||||
String email = body.getString(JSON_KEY_EMAIL);
|
||||
Boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
|
||||
delegate.handleSuccess(new StatusResponse(email, verified));
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
delegate.handleError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
resource.get();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void sign(final byte[] sessionToken, final ExtendedJSONObject publicKey, long durationInSeconds, final RequestDelegate<String> delegate) {
|
||||
final JSONObject body = new JSONObject();
|
||||
body.put("publicKey", publicKey);
|
||||
body.put("duration", durationInSeconds);
|
||||
|
||||
final byte[] tokenId = new byte[32];
|
||||
final byte[] reqHMACKey = new byte[32];
|
||||
try {
|
||||
HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
|
||||
} catch (Exception e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
BaseResource resource;
|
||||
try {
|
||||
resource = new BaseResource(new URI(serverURI + "certificate/sign"));
|
||||
} catch (URISyntaxException e) {
|
||||
invokeHandleError(delegate, e);
|
||||
return;
|
||||
}
|
||||
|
||||
resource.delegate = new ResourceDelegate<String>(resource, delegate, tokenId, reqHMACKey, true) {
|
||||
@Override
|
||||
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
|
||||
String cert = body.getString("cert");
|
||||
if (cert == null) {
|
||||
delegate.handleError(new FxAccountClientException("cert must be a non-null string"));
|
||||
return;
|
||||
}
|
||||
delegate.handleSuccess(cert);
|
||||
}
|
||||
};
|
||||
post(resource, body, delegate);
|
||||
}
|
||||
public interface FxAccountClient {
|
||||
public void loginAndGetKeys(final byte[] emailUTF8, final byte[] quickStretchedPW, final RequestDelegate<LoginResponse> requestDelegate);
|
||||
public void status(byte[] sessionToken, RequestDelegate<StatusResponse> requestDelegate);
|
||||
public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
|
||||
public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import org.mozilla.gecko.sync.net.BaseResource;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
|
||||
public class FxAccountClient20 extends FxAccountClient10 {
|
||||
public class FxAccountClient20 extends FxAccountClient10 implements FxAccountClient {
|
||||
protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN };
|
||||
protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN, JSON_KEY_KEYFETCHTOKEN, };
|
||||
protected static final String[] LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS = new String[] { JSON_KEY_VERIFIED };
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
package org.mozilla.gecko.fxa;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
|
||||
public class FxAccountConstants {
|
||||
public static final String GLOBAL_LOG_TAG = "FxAccounts";
|
||||
public static final String ACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@";
|
||||
@ -13,4 +15,14 @@ public class FxAccountConstants {
|
||||
public static final String DEFAULT_AUTH_ENDPOINT = "http://auth.oldsync.dev.lcip.org";
|
||||
|
||||
public static final String PREFS_PATH = "fxa.v1";
|
||||
|
||||
// For extra debugging. Not final so it can be changed from Fennec, or from
|
||||
// an add-on.
|
||||
public static boolean LOG_PERSONAL_INFORMATION = true;
|
||||
|
||||
public static void pii(String tag, String message) {
|
||||
if (LOG_PERSONAL_INFORMATION) {
|
||||
Logger.info(tag, "$$FxA PII$$: " + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,11 +40,14 @@ public interface AbstractFxAccount {
|
||||
*/
|
||||
public String getServerURI();
|
||||
|
||||
public boolean isValid();
|
||||
public void setInvalid();
|
||||
|
||||
public byte[] getSessionToken();
|
||||
public byte[] getKeyFetchToken();
|
||||
|
||||
public void invalidateSessionToken();
|
||||
public void invalidateKeyFetchToken();
|
||||
public void setSessionToken(byte[] token);
|
||||
public void setKeyFetchToken(byte[] token);
|
||||
|
||||
/**
|
||||
* Return true if and only if this account is guaranteed to be verified. This
|
||||
@ -77,4 +80,13 @@ public interface AbstractFxAccount {
|
||||
public void setWrappedKb(byte[] wrappedKb);
|
||||
|
||||
BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException;
|
||||
|
||||
public String getCertificate();
|
||||
public void setCertificate(String certificate);
|
||||
|
||||
public String getAssertion();
|
||||
public void setAssertion(String assertion);
|
||||
|
||||
public byte[] getEmailUTF8();
|
||||
public byte[] getQuickStretchedPW();
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ package org.mozilla.gecko.fxa.authenticator;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountUtils;
|
||||
@ -31,6 +33,9 @@ import android.os.Bundle;
|
||||
public class AndroidFxAccount implements AbstractFxAccount {
|
||||
protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
|
||||
|
||||
public static final String ACCOUNT_KEY_ASSERTION = "assertion";
|
||||
public static final String ACCOUNT_KEY_CERTIFICATE = "certificate";
|
||||
public static final String ACCOUNT_KEY_INVALID = "invalid";
|
||||
public static final String ACCOUNT_KEY_SERVERURI = "serverURI";
|
||||
public static final String ACCOUNT_KEY_SESSION_TOKEN = "sessionToken";
|
||||
public static final String ACCOUNT_KEY_KEY_FETCH_TOKEN = "keyFetchToken";
|
||||
@ -66,6 +71,21 @@ public class AndroidFxAccount implements AbstractFxAccount {
|
||||
this.accountManager = AccountManager.get(this.context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEmailUTF8() {
|
||||
try {
|
||||
return account.name.getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Ignore.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getQuickStretchedPW() {
|
||||
return Utils.hex2Byte(accountManager.getPassword(account));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServerURI() {
|
||||
return accountManager.getUserData(account, ACCOUNT_KEY_SERVERURI);
|
||||
@ -90,13 +110,13 @@ public class AndroidFxAccount implements AbstractFxAccount {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateSessionToken() {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, null);
|
||||
public void setSessionToken(byte[] sessionToken) {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateKeyFetchToken() {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, null);
|
||||
public void setKeyFetchToken(byte[] keyFetchToken) {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -155,6 +175,26 @@ public class AndroidFxAccount implements AbstractFxAccount {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCertificate() {
|
||||
return accountManager.getUserData(account, ACCOUNT_KEY_CERTIFICATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCertificate(String certificate) {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_CERTIFICATE, certificate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAssertion() {
|
||||
return accountManager.getUserData(account, ACCOUNT_KEY_ASSERTION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAssertion(String assertion) {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_ASSERTION, assertion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a JSON dictionary of the string values associated to this account.
|
||||
* <p>
|
||||
@ -167,16 +207,27 @@ public class AndroidFxAccount implements AbstractFxAccount {
|
||||
public ExtendedJSONObject toJSONObject() {
|
||||
ExtendedJSONObject o = new ExtendedJSONObject();
|
||||
for (String key : new String[] {
|
||||
ACCOUNT_KEY_ASSERTION,
|
||||
ACCOUNT_KEY_CERTIFICATE,
|
||||
ACCOUNT_KEY_SERVERURI,
|
||||
ACCOUNT_KEY_SESSION_TOKEN,
|
||||
ACCOUNT_KEY_INVALID,
|
||||
ACCOUNT_KEY_KEY_FETCH_TOKEN,
|
||||
ACCOUNT_KEY_VERIFIED,
|
||||
ACCOUNT_KEY_KA,
|
||||
ACCOUNT_KEY_KB,
|
||||
ACCOUNT_KEY_UNWRAPKB,
|
||||
ACCOUNT_KEY_ASSERTION_KEY_PAIR }) {
|
||||
ACCOUNT_KEY_ASSERTION_KEY_PAIR,
|
||||
}) {
|
||||
o.put(key, accountManager.getUserData(account, key));
|
||||
}
|
||||
o.put("email", account.name);
|
||||
try {
|
||||
o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Ignore.
|
||||
}
|
||||
o.put("quickStretchedPW", accountManager.getPassword(account));
|
||||
return o;
|
||||
}
|
||||
|
||||
@ -208,8 +259,8 @@ public class AndroidFxAccount implements AbstractFxAccount {
|
||||
|
||||
Bundle userdata = new Bundle();
|
||||
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SERVERURI, serverURI);
|
||||
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SESSION_TOKEN, Utils.byte2Hex(sessionToken));
|
||||
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_KEY_FETCH_TOKEN, Utils.byte2Hex(keyFetchToken));
|
||||
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken));
|
||||
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken));
|
||||
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_VERIFIED, Boolean.valueOf(verified).toString());
|
||||
userdata.putString(AndroidFxAccount.ACCOUNT_KEY_UNWRAPKB, Utils.byte2Hex(unwrapBkey));
|
||||
|
||||
@ -222,4 +273,40 @@ public class AndroidFxAccount implements AbstractFxAccount {
|
||||
FxAccountAuthenticator.enableSyncing(context, account);
|
||||
return account;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
// Boolean.valueOf only returns true for the string "true"; this errors in
|
||||
// the direction of marking accounts valid.
|
||||
boolean invalid = Boolean.valueOf(accountManager.getUserData(account, ACCOUNT_KEY_INVALID)).booleanValue();
|
||||
return !invalid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInvalid() {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_INVALID, Boolean.valueOf(true).toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>For debugging only!</b>
|
||||
*/
|
||||
public void dump() {
|
||||
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
|
||||
return;
|
||||
}
|
||||
ExtendedJSONObject o = toJSONObject();
|
||||
ArrayList<String> list = new ArrayList<String>(o.keySet());
|
||||
Collections.sort(list);
|
||||
for (String key : list) {
|
||||
FxAccountConstants.pii(LOG_TAG, key + ": " + o.getString(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>For debugging only!</b>
|
||||
*/
|
||||
public void resetAccountTokens() {
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, null);
|
||||
accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, null);
|
||||
}
|
||||
}
|
||||
|
@ -4,23 +4,25 @@
|
||||
|
||||
package org.mozilla.gecko.fxa.authenticator;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountClient;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountClient10;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
|
||||
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.browserid.BrowserIDKeyPair;
|
||||
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
|
||||
import org.mozilla.gecko.browserid.SigningPrivateKey;
|
||||
import org.mozilla.gecko.browserid.VerifyingPublicKey;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException.FxAccountLoginAccountNotVerifiedException;
|
||||
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException.FxAccountLoginBadPasswordException;
|
||||
import org.mozilla.gecko.sync.HTTPFailureException;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
import org.mozilla.gecko.sync.net.SyncStorageResponse;
|
||||
|
||||
import android.content.Context;
|
||||
@ -39,13 +41,130 @@ public class FxAccountLoginPolicy {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
protected void invokeHandleHardFailure(final FxAccountLoginDelegate delegate, final FxAccountLoginException e) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleError(e);
|
||||
}
|
||||
});
|
||||
public long certificateDurationInMilliseconds = JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
|
||||
public long assertionDurationInMilliseconds = JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS;
|
||||
|
||||
public long getCertificateDurationInMilliseconds() {
|
||||
return certificateDurationInMilliseconds;
|
||||
}
|
||||
|
||||
public long getAssertionDurationInMilliseconds() {
|
||||
return assertionDurationInMilliseconds;
|
||||
}
|
||||
|
||||
protected FxAccountClient makeFxAccountClient() {
|
||||
String serverURI = fxAccount.getServerURI();
|
||||
return new FxAccountClient20(serverURI, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this certificate is not worth generating an assertion from: for
|
||||
* example, because it is not well-formed, or it is already expired.
|
||||
*
|
||||
* @param certificate
|
||||
* to check.
|
||||
* @return if it is definitely not worth generating an assertion from this
|
||||
* certificate.
|
||||
*/
|
||||
protected boolean isInvalidCertificate(String certificate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this assertion is not worth presenting to the token server: for
|
||||
* example, because it is not well-formed, or it is already expired.
|
||||
*
|
||||
* @param assertion
|
||||
* to check.
|
||||
* @return if assertion is definitely not worth presenting to the token
|
||||
* server.
|
||||
*/
|
||||
protected boolean isInvalidAssertion(String assertion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public enum AccountState {
|
||||
Invalid,
|
||||
NeedsSessionToken,
|
||||
NeedsVerification,
|
||||
NeedsKeys,
|
||||
NeedsCertificate,
|
||||
NeedsAssertion,
|
||||
Valid,
|
||||
};
|
||||
|
||||
public AccountState getAccountState(AbstractFxAccount fxAccount) {
|
||||
String serverURI = fxAccount.getServerURI();
|
||||
byte[] emailUTF8 = fxAccount.getEmailUTF8();
|
||||
byte[] quickStretchedPW = fxAccount.getQuickStretchedPW();
|
||||
if (!fxAccount.isValid() || serverURI == null || emailUTF8 == null || quickStretchedPW == null) {
|
||||
return AccountState.Invalid;
|
||||
}
|
||||
|
||||
byte[] sessionToken = fxAccount.getSessionToken();
|
||||
if (sessionToken == null) {
|
||||
return AccountState.NeedsSessionToken;
|
||||
}
|
||||
|
||||
if (!fxAccount.isVerified()) {
|
||||
return AccountState.NeedsVerification;
|
||||
}
|
||||
|
||||
// Verify against server? Tricky.
|
||||
if (fxAccount.getKa() == null || fxAccount.getKb() == null) {
|
||||
return AccountState.NeedsKeys;
|
||||
}
|
||||
|
||||
String certificate = fxAccount.getCertificate();
|
||||
if (certificate == null || isInvalidCertificate(certificate)) {
|
||||
return AccountState.NeedsCertificate;
|
||||
}
|
||||
|
||||
String assertion = fxAccount.getAssertion();
|
||||
if (assertion == null || isInvalidAssertion(assertion)) {
|
||||
return AccountState.NeedsAssertion;
|
||||
}
|
||||
|
||||
return AccountState.Valid;
|
||||
}
|
||||
|
||||
protected interface LoginStage {
|
||||
public void execute(LoginStageDelegate delegate) throws Exception;
|
||||
}
|
||||
|
||||
protected LinkedList<LoginStage> getStages(AccountState state) {
|
||||
final LinkedList<LoginStage> stages = new LinkedList<LoginStage>();
|
||||
if (state == AccountState.Invalid) {
|
||||
stages.addFirst(new FailStage());
|
||||
return stages;
|
||||
}
|
||||
|
||||
stages.addFirst(new SuccessStage());
|
||||
if (state == AccountState.Valid) {
|
||||
return stages;
|
||||
}
|
||||
stages.addFirst(new EnsureAssertionStage());
|
||||
if (state == AccountState.NeedsAssertion) {
|
||||
return stages;
|
||||
}
|
||||
stages.addFirst(new EnsureCertificateStage());
|
||||
if (state == AccountState.NeedsCertificate) {
|
||||
return stages;
|
||||
}
|
||||
stages.addFirst(new EnsureKeysStage());
|
||||
stages.addFirst(new EnsureKeyFetchTokenStage());
|
||||
if (state == AccountState.NeedsKeys) {
|
||||
return stages;
|
||||
}
|
||||
stages.addFirst(new EnsureVerificationStage());
|
||||
if (state == AccountState.NeedsVerification) {
|
||||
return stages;
|
||||
}
|
||||
stages.addFirst(new EnsureSessionTokenStage());
|
||||
if (state == AccountState.NeedsSessionToken) {
|
||||
return stages;
|
||||
}
|
||||
return stages;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,121 +177,97 @@ public class FxAccountLoginPolicy {
|
||||
* @param delegate providing callbacks to invoke.
|
||||
*/
|
||||
public void login(final String audience, final FxAccountLoginDelegate delegate) {
|
||||
final LinkedList<LoginStage> stages = new LinkedList<LoginStage>();
|
||||
stages.add(new CheckPreconditionsLoginStage());
|
||||
stages.add(new CheckVerifiedLoginStage());
|
||||
stages.add(new EnsureDerivedKeysLoginStage());
|
||||
stages.add(new FetchCertificateLoginStage());
|
||||
final AccountState initialState = getAccountState(fxAccount);
|
||||
Logger.info(LOG_TAG, "Logging in account from initial state " + initialState + ".");
|
||||
|
||||
advance(audience, stages, delegate);
|
||||
final LinkedList<LoginStage> stages = getStages(initialState);
|
||||
final LinkedList<String> stageNames = new LinkedList<String>();
|
||||
for (LoginStage stage : stages) {
|
||||
stageNames.add(stage.getClass().getSimpleName());
|
||||
}
|
||||
Logger.info(LOG_TAG, "Executing stages: [" + Utils.toCommaSeparatedString(stageNames) + "]");
|
||||
|
||||
LoginStageDelegate loginStageDelegate = new LoginStageDelegate(stages, audience, delegate);
|
||||
loginStageDelegate.advance();
|
||||
}
|
||||
|
||||
protected interface LoginStageDelegate {
|
||||
public String getAssertionAudience();
|
||||
public void handleError(FxAccountLoginException e);
|
||||
public void handleStageSuccess();
|
||||
public void handleLoginSuccess(String assertion);
|
||||
}
|
||||
protected class LoginStageDelegate {
|
||||
public final LinkedList<LoginStage> stages;
|
||||
public final String audience;
|
||||
public final FxAccountLoginDelegate delegate;
|
||||
public final FxAccountClient client;
|
||||
|
||||
protected interface LoginStage {
|
||||
public void execute(LoginStageDelegate delegate);
|
||||
}
|
||||
protected LoginStage currentStage = null;
|
||||
|
||||
/**
|
||||
* Pop the next stage off <code>stages</code> and execute it.
|
||||
* <p>
|
||||
* This trades stack efficiency for implementation simplicity.
|
||||
*
|
||||
* @param delegate
|
||||
* @param stages
|
||||
*/
|
||||
protected void advance(final String audience, final LinkedList<LoginStage> stages, final FxAccountLoginDelegate delegate) {
|
||||
LoginStage stage = stages.poll();
|
||||
if (stage == null) {
|
||||
// No more stages. But we haven't seen an assertion. Failure!
|
||||
Logger.info(LOG_TAG, "No more stages: login failed?");
|
||||
invokeHandleHardFailure(delegate, new FxAccountLoginException("No more stages, but no assertion: login failed?"));
|
||||
public LoginStageDelegate(LinkedList<LoginStage> stages, String audience, FxAccountLoginDelegate delegate) {
|
||||
this.stages = stages;
|
||||
this.audience = audience;
|
||||
this.delegate = delegate;
|
||||
this.client = makeFxAccountClient();
|
||||
}
|
||||
|
||||
protected void invokeHandleHardFailure(final FxAccountLoginDelegate delegate, final FxAccountLoginException e) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void advance() {
|
||||
currentStage = stages.poll();
|
||||
if (currentStage == null) {
|
||||
// No more stages. But we haven't seen an assertion. Failure!
|
||||
Logger.info(LOG_TAG, "No more stages: login failed?");
|
||||
invokeHandleHardFailure(delegate, new FxAccountLoginException("No more stages, but no assertion: login failed?"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Logger.info(LOG_TAG, "Executing stage: " + currentStage.getClass().getSimpleName());
|
||||
currentStage.execute(this);
|
||||
} catch (Exception e) {
|
||||
Logger.info(LOG_TAG, "Got exception during stage.", e);
|
||||
invokeHandleHardFailure(delegate, new FxAccountLoginException(e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleStageSuccess() {
|
||||
Logger.info(LOG_TAG, "Stage succeeded: " + currentStage.getClass().getSimpleName());
|
||||
advance();
|
||||
}
|
||||
|
||||
public void handleLoginSuccess(final String assertion) {
|
||||
Logger.info(LOG_TAG, "Login succeeded.");
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleSuccess(assertion);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
stage.execute(new LoginStageDelegate() {
|
||||
@Override
|
||||
public void handleStageSuccess() {
|
||||
Logger.info(LOG_TAG, "Stage succeeded.");
|
||||
advance(audience, stages, delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleLoginSuccess(final String assertion) {
|
||||
Logger.info(LOG_TAG, "Login succeeded.");
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleSuccess(assertion);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(FxAccountLoginException e) {
|
||||
invokeHandleHardFailure(delegate, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAssertionAudience() {
|
||||
return audience;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify we have a valid server URI, session token, etc. If not, we have to
|
||||
* prompt for credentials.
|
||||
*/
|
||||
public class CheckPreconditionsLoginStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) {
|
||||
final String audience = delegate.getAssertionAudience();
|
||||
if (audience == null) {
|
||||
delegate.handleError(new FxAccountLoginException("Account has no audience."));
|
||||
return;
|
||||
}
|
||||
|
||||
String serverURI = fxAccount.getServerURI();
|
||||
if (serverURI == null) {
|
||||
delegate.handleError(new FxAccountLoginException("Account has no server URI."));
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] sessionToken = fxAccount.getSessionToken();
|
||||
if (sessionToken == null) {
|
||||
delegate.handleError(new FxAccountLoginBadPasswordException("Account has no session token."));
|
||||
return;
|
||||
}
|
||||
|
||||
delegate.handleStageSuccess();
|
||||
public void handleError(FxAccountLoginException e) {
|
||||
invokeHandleHardFailure(delegate, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now that we have a server to talk to and a session token, we can use them
|
||||
* to check that the account is verified.
|
||||
*/
|
||||
public class CheckVerifiedLoginStage implements LoginStage {
|
||||
public class EnsureSessionTokenStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) {
|
||||
if (fxAccount.isVerified()) {
|
||||
Logger.info(LOG_TAG, "Account is already marked verified. Skipping remote status check.");
|
||||
delegate.handleStageSuccess();
|
||||
return;
|
||||
public void execute(final LoginStageDelegate delegate) throws Exception {
|
||||
byte[] emailUTF8 = fxAccount.getEmailUTF8();
|
||||
if (emailUTF8 == null) {
|
||||
throw new IllegalStateException("emailUTF8 must not be null");
|
||||
}
|
||||
byte[] quickStretchedPW = fxAccount.getQuickStretchedPW();
|
||||
if (quickStretchedPW == null) {
|
||||
throw new IllegalStateException("quickStretchedPW must not be null");
|
||||
}
|
||||
|
||||
String serverURI = fxAccount.getServerURI();
|
||||
byte[] sessionToken = fxAccount.getSessionToken();
|
||||
final FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
|
||||
|
||||
client.status(sessionToken, new RequestDelegate<StatusResponse>() {
|
||||
delegate.client.loginAndGetKeys(emailUTF8, quickStretchedPW, new RequestDelegate<FxAccountClient20.LoginResponse>() {
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
delegate.handleError(new FxAccountLoginException(e));
|
||||
@ -184,6 +279,53 @@ public class FxAccountLoginPolicy {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
}
|
||||
// We just got denied for a sessionToken. That's a problem with
|
||||
// our email or password. Only thing to do is mark the account
|
||||
// invalid and ask for user intervention.
|
||||
fxAccount.setInvalid();
|
||||
delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected email/password while fetching sessionToken."));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSuccess(LoginResponse result) {
|
||||
fxAccount.setSessionToken(result.sessionToken);
|
||||
fxAccount.setKeyFetchToken(result.keyFetchToken);
|
||||
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
|
||||
FxAccountConstants.pii(LOG_TAG, "Fetched sessionToken : " + Utils.byte2Hex(result.sessionToken));
|
||||
FxAccountConstants.pii(LOG_TAG, "Fetched keyFetchToken: " + Utils.byte2Hex(result.keyFetchToken));
|
||||
}
|
||||
delegate.handleStageSuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now that we have a server to talk to and a session token, we can use them
|
||||
* to check that the account is verified.
|
||||
*/
|
||||
public class EnsureVerificationStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) {
|
||||
byte[] sessionToken = fxAccount.getSessionToken();
|
||||
if (sessionToken == null) {
|
||||
throw new IllegalArgumentException("sessionToken must not be null");
|
||||
}
|
||||
|
||||
delegate.client.status(sessionToken, new RequestDelegate<StatusResponse>() {
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
delegate.handleError(new FxAccountLoginException(e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFailure(int status, HttpResponse response) {
|
||||
if (status != 401) {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
}
|
||||
// We just got denied due to our sessionToken. Invalidate it.
|
||||
fxAccount.setSessionToken(null);
|
||||
delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected session token while fetching status."));
|
||||
}
|
||||
|
||||
@ -202,31 +344,81 @@ public class FxAccountLoginPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we have a verified account, we can make sure that our local keys are
|
||||
* consistent with the account's keys.
|
||||
*/
|
||||
public class EnsureDerivedKeysLoginStage implements LoginStage {
|
||||
public static int[] DUMMY = null;
|
||||
|
||||
public class EnsureKeyFetchTokenStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) {
|
||||
byte[] kA = fxAccount.getKa();
|
||||
byte[] kB = fxAccount.getKb();
|
||||
if (kA != null && kB != null) {
|
||||
Logger.info(LOG_TAG, "Account already has kA and kB. Skipping key derivation stage.");
|
||||
public void execute(final LoginStageDelegate delegate) throws Exception {
|
||||
byte[] emailUTF8 = fxAccount.getEmailUTF8();
|
||||
if (emailUTF8 == null) {
|
||||
throw new IllegalStateException("emailUTF8 must not be null");
|
||||
}
|
||||
byte[] quickStretchedPW = fxAccount.getQuickStretchedPW();
|
||||
if (quickStretchedPW == null) {
|
||||
throw new IllegalStateException("quickStretchedPW must not be null");
|
||||
}
|
||||
|
||||
boolean verified = fxAccount.isVerified();
|
||||
if (!verified) {
|
||||
throw new IllegalStateException("must be verified");
|
||||
}
|
||||
|
||||
// We might already have a valid keyFetchToken. If so, try it. If it's not
|
||||
// valid, we'll invalidate it in EnsureKeysStage.
|
||||
if (fxAccount.getKeyFetchToken() != null) {
|
||||
Logger.info(LOG_TAG, "Using existing keyFetchToken.");
|
||||
delegate.handleStageSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
delegate.client.loginAndGetKeys(emailUTF8, quickStretchedPW, new RequestDelegate<FxAccountClient20.LoginResponse>() {
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
delegate.handleError(new FxAccountLoginException(e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFailure(int status, HttpResponse response) {
|
||||
if (status != 401) {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
}
|
||||
// We just got denied for a keyFetchToken. That's a problem with
|
||||
// our email or password. Only thing to do is mark the account
|
||||
// invalid and ask for user intervention.
|
||||
fxAccount.setInvalid();
|
||||
delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected email/password while fetching keyFetchToken."));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSuccess(LoginResponse result) {
|
||||
fxAccount.setKeyFetchToken(result.keyFetchToken);
|
||||
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
|
||||
FxAccountConstants.pii(LOG_TAG, "Fetched keyFetchToken: " + Utils.byte2Hex(result.keyFetchToken));
|
||||
}
|
||||
delegate.handleStageSuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we have a verified account, we can make sure that our local keys are
|
||||
* consistent with the account's keys.
|
||||
*/
|
||||
public class EnsureKeysStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) throws Exception {
|
||||
byte[] keyFetchToken = fxAccount.getKeyFetchToken();
|
||||
if (keyFetchToken == null) {
|
||||
// XXX this might mean something else?
|
||||
delegate.handleError(new FxAccountLoginBadPasswordException("Account has no key fetch token."));
|
||||
return;
|
||||
throw new IllegalStateException("keyFetchToken must not be null");
|
||||
}
|
||||
|
||||
String serverURI = fxAccount.getServerURI();
|
||||
final FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
|
||||
client.keys(keyFetchToken, new RequestDelegate<FxAccountClient10.TwoKeys>() {
|
||||
// Make sure we don't use a keyFetchToken twice. This conveniently
|
||||
// invalidates any invalid keyFetchToken we might try, too.
|
||||
fxAccount.setKeyFetchToken(null);
|
||||
|
||||
delegate.client.keys(keyFetchToken, new RequestDelegate<FxAccountClient10.TwoKeys>() {
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
delegate.handleError(new FxAccountLoginException(e));
|
||||
@ -245,39 +437,35 @@ public class FxAccountLoginPolicy {
|
||||
public void handleSuccess(TwoKeys result) {
|
||||
fxAccount.setKa(result.kA);
|
||||
fxAccount.setWrappedKb(result.wrapkB);
|
||||
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
|
||||
FxAccountConstants.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA));
|
||||
FxAccountConstants.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB));
|
||||
FxAccountConstants.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(fxAccount.getKb()));
|
||||
}
|
||||
delegate.handleStageSuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class FetchCertificateLoginStage implements LoginStage {
|
||||
public class EnsureCertificateStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) {
|
||||
BrowserIDKeyPair keyPair;
|
||||
try {
|
||||
keyPair = fxAccount.getAssertionKeyPair();
|
||||
if (keyPair == null) {
|
||||
Logger.info(LOG_TAG, "Account has no key pair.");
|
||||
delegate.handleError(new FxAccountLoginException("Account has no key pair."));
|
||||
return;
|
||||
}
|
||||
} catch (GeneralSecurityException e) {
|
||||
delegate.handleError(new FxAccountLoginException(e));
|
||||
return;
|
||||
public void execute(final LoginStageDelegate delegate) throws Exception{
|
||||
byte[] sessionToken = fxAccount.getSessionToken();
|
||||
if (sessionToken == null) {
|
||||
throw new IllegalStateException("keyPair must not be null");
|
||||
}
|
||||
BrowserIDKeyPair keyPair = fxAccount.getAssertionKeyPair();
|
||||
if (keyPair == null) {
|
||||
// If we can't fetch a keypair, we probably have some crypto
|
||||
// configuration error on device, which we are never going to recover
|
||||
// from. Mark the account invalid.
|
||||
fxAccount.setInvalid();
|
||||
throw new IllegalStateException("keyPair must not be null");
|
||||
}
|
||||
|
||||
final SigningPrivateKey privateKey = keyPair.getPrivate();
|
||||
final VerifyingPublicKey publicKey = keyPair.getPublic();
|
||||
|
||||
byte[] sessionToken = fxAccount.getSessionToken();
|
||||
String serverURI = fxAccount.getServerURI();
|
||||
final FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
|
||||
|
||||
// TODO Make this duration configurable (that is, part of the policy).
|
||||
long certificateDurationInMilliseconds = JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
|
||||
|
||||
client.sign(sessionToken, publicKey.toJSONObject(), certificateDurationInMilliseconds, new RequestDelegate<String>() {
|
||||
delegate.client.sign(sessionToken, publicKey.toJSONObject(), getCertificateDurationInMilliseconds(), new RequestDelegate<String>() {
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
delegate.handleError(new FxAccountLoginException(e));
|
||||
@ -289,24 +477,78 @@ public class FxAccountLoginPolicy {
|
||||
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
|
||||
return;
|
||||
}
|
||||
// Our sessionToken was just rejected; we should get a new
|
||||
// sessionToken. TODO: Make sure the exception below is fine
|
||||
// enough grained.
|
||||
// Since this is the place we'll see the majority of lifecylcle
|
||||
// auth problems, we should be much more aggressive bumping the
|
||||
// state machine out of this state when we don't get success.
|
||||
fxAccount.setSessionToken(null);
|
||||
delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected session token while fetching status."));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSuccess(String certificate) {
|
||||
try {
|
||||
String assertion = JSONWebTokenUtils.createAssertion(privateKey, certificate, delegate.getAssertionAudience());
|
||||
if (Logger.LOG_PERSONAL_INFORMATION) {
|
||||
Logger.pii(LOG_TAG, "Generated assertion " + assertion);
|
||||
JSONWebTokenUtils.dumpAssertion(assertion);
|
||||
}
|
||||
delegate.handleLoginSuccess(assertion);
|
||||
} catch (Exception e) {
|
||||
delegate.handleError(new FxAccountLoginException(e));
|
||||
return;
|
||||
fxAccount.setCertificate(certificate);
|
||||
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
|
||||
FxAccountConstants.pii(LOG_TAG, "Fetched certificate: " + certificate);
|
||||
JSONWebTokenUtils.dumpCertificate(certificate);
|
||||
}
|
||||
delegate.handleStageSuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class EnsureAssertionStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) throws Exception {
|
||||
BrowserIDKeyPair keyPair = fxAccount.getAssertionKeyPair();
|
||||
if (keyPair == null) {
|
||||
throw new IllegalStateException("keyPair must not be null");
|
||||
}
|
||||
String certificate = fxAccount.getCertificate();
|
||||
if (certificate == null) {
|
||||
throw new IllegalStateException("certificate must not be null");
|
||||
}
|
||||
String assertion;
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, delegate.audience,
|
||||
JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, now, getAssertionDurationInMilliseconds());
|
||||
} catch (Exception e) {
|
||||
// If we can't sign an assertion, we probably have some crypto
|
||||
// configuration error on device, which we are never going to recover
|
||||
// from. Mark the account invalid before raising the exception.
|
||||
fxAccount.setInvalid();
|
||||
throw e;
|
||||
}
|
||||
fxAccount.setAssertion(assertion);
|
||||
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
|
||||
FxAccountConstants.pii(LOG_TAG, "Generated assertion: " + assertion);
|
||||
JSONWebTokenUtils.dumpAssertion(assertion);
|
||||
}
|
||||
delegate.handleStageSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
public class SuccessStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) throws Exception {
|
||||
String assertion = fxAccount.getAssertion();
|
||||
if (assertion == null) {
|
||||
throw new IllegalStateException("assertion must not be null");
|
||||
}
|
||||
delegate.handleLoginSuccess(assertion);
|
||||
}
|
||||
}
|
||||
|
||||
public class FailStage implements LoginStage {
|
||||
@Override
|
||||
public void execute(final LoginStageDelegate delegate) {
|
||||
AccountState finalState = getAccountState(fxAccount);
|
||||
Logger.info(LOG_TAG, "Failed to login account; final state is " + finalState + ".");
|
||||
delegate.handleError(new FxAccountLoginException("Failed to login."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.sync.GlobalSession;
|
||||
import org.mozilla.gecko.sync.NonObjectJSONException;
|
||||
import org.mozilla.gecko.sync.SyncConfigurationException;
|
||||
@ -39,7 +39,7 @@ public class FxAccountGlobalSession extends GlobalSession {
|
||||
callback, context, extras, clientsDelegate, null);
|
||||
URI uri = new URI(storageEndpoint);
|
||||
this.config.clusterURL = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), "/", null, null);
|
||||
Logger.warn(LOG_TAG, "storageEndpoint is " + uri + " and clusterURL is " + config.clusterURL);
|
||||
FxAccountConstants.pii(LOG_TAG, "storageEndpoint is " + uri + " and clusterURL is " + config.clusterURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -5,14 +5,14 @@
|
||||
package org.mozilla.gecko.fxa.sync;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.fxa.FxAccountUtils;
|
||||
import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient;
|
||||
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
|
||||
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
|
||||
@ -129,16 +129,16 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
final AndroidFxAccount fxAccount = new AndroidFxAccount(getContext(), account);
|
||||
|
||||
if (Logger.LOG_PERSONAL_INFORMATION) {
|
||||
ExtendedJSONObject o = new AndroidFxAccount(getContext(), account).toJSONObject();
|
||||
ArrayList<String> list = new ArrayList<String>(o.keySet());
|
||||
Collections.sort(list);
|
||||
for (String key : list) {
|
||||
Logger.pii(LOG_TAG, key + ": " + o.getString(key));
|
||||
}
|
||||
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
|
||||
fxAccount.dump();
|
||||
}
|
||||
|
||||
final SharedPreferences sharedPrefs = getContext().getSharedPreferences(FxAccountConstants.PREFS_PATH, Context.MODE_PRIVATE); // TODO Ensure preferences are per-Account.
|
||||
|
||||
final FxAccountLoginPolicy loginPolicy = new FxAccountLoginPolicy(getContext(), fxAccount, executor);
|
||||
loginPolicy.certificateDurationInMilliseconds = 20 * 60 * 1000;
|
||||
loginPolicy.assertionDurationInMilliseconds = 15 * 60 * 1000;
|
||||
Logger.info(LOG_TAG, "Asking for certificates to expire after 20 minutes and assertions to expire after 15 minutes.");
|
||||
|
||||
loginPolicy.login(authEndpoint, new FxAccountLoginDelegate() {
|
||||
@Override
|
||||
@ -147,12 +147,12 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new TokenServerClientDelegate() {
|
||||
@Override
|
||||
public void handleSuccess(final TokenServerToken token) {
|
||||
Logger.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
|
||||
FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
|
||||
sharedPrefs.edit().putLong("tokenFailures", 0).commit();
|
||||
|
||||
final BaseGlobalSessionCallback callback = new SessionCallback(latch);
|
||||
FxAccountGlobalSession globalSession = null;
|
||||
try {
|
||||
SharedPreferences sharedPrefs = getContext().getSharedPreferences(FxAccountConstants.PREFS_PATH, Context.MODE_PRIVATE);
|
||||
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);
|
||||
@ -166,6 +166,21 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
@Override
|
||||
public void handleFailure(TokenServerException e) {
|
||||
// This is tricky since the token server fairly
|
||||
// consistently rejects a token the first time it sees it
|
||||
// before accepting it for the rest of its lifetime.
|
||||
long MAX_TOKEN_FAILURES_PER_TOKEN = 2;
|
||||
long tokenFailures = 1 + sharedPrefs.getLong("tokenFailures", 0);
|
||||
if (tokenFailures > MAX_TOKEN_FAILURES_PER_TOKEN) {
|
||||
fxAccount.setCertificate(null);
|
||||
tokenFailures = 0;
|
||||
Logger.warn(LOG_TAG, "Seen too many failures with this token; resetting: " + tokenFailures);
|
||||
Logger.warn(LOG_TAG, "To aid debugging, synchronously sending assertion to remote verifier for second look.");
|
||||
debugAssertion(tokenServerEndpoint, assertion);
|
||||
} else {
|
||||
Logger.info(LOG_TAG, "Seen " + tokenFailures + " failures with this token so far.");
|
||||
}
|
||||
sharedPrefs.edit().putLong("tokenFailures", tokenFailures).commit();
|
||||
handleError(e);
|
||||
}
|
||||
|
||||
@ -190,4 +205,34 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
protected void debugAssertion(String audience, String assertion) {
|
||||
final CountDownLatch verifierLatch = new CountDownLatch(1);
|
||||
BrowserIDRemoteVerifierClient client = new BrowserIDRemoteVerifierClient(URI.create(BrowserIDRemoteVerifierClient.DEFAULT_VERIFIER_URL));
|
||||
client.verify(audience, assertion, new BrowserIDVerifierDelegate() {
|
||||
@Override
|
||||
public void handleSuccess(ExtendedJSONObject response) {
|
||||
Logger.info(LOG_TAG, "Remote verifier returned success: " + response.toJSONString());
|
||||
verifierLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFailure(ExtendedJSONObject response) {
|
||||
Logger.warn(LOG_TAG, "Remote verifier returned failure: " + response.toJSONString());
|
||||
verifierLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
Logger.error(LOG_TAG, "Remote verifier returned error.", e);
|
||||
verifierLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
verifierLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Logger.error(LOG_TAG, "Got error.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<style name="FxAccountTheme" parent="@style/Gecko" />
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user