diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild
index c74bea63556..49a198a2119 100644
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -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',
diff --git a/mobile/android/base/background/fxa/FxAccountClient.java b/mobile/android/base/background/fxa/FxAccountClient.java
index 3f3e287f5b9..635b6a9d611 100644
--- a/mobile/android/base/background/fxa/FxAccountClient.java
+++ b/mobile/android/base/background/fxa/FxAccountClient.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.
- *
- * The reference server is developed at
- * https://github.com/mozilla/picl-idp.
- * This implementation was developed against
- * https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208.
- *
- * The delegate structure used is a little different from the rest of the code
- * base. We add a RequestDelegate
layer that processes a typed
- * value extracted from the body of a successful response.
- *
- * Further, we add internal CreateDelegate
and
- * AuthDelegate
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 {
- public void handleError(Exception e);
- public void handleFailure(int status, HttpResponse response);
- public void handleSuccess(T result);
- }
-
- /**
- * A CreateDelegate
produces the body of a /create request.
- */
- public interface CreateDelegate {
- public JSONObject getCreateBody() throws FxAccountClientException;
- }
-
- /**
- * A AuthDelegate
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 void invokeHandleError(final RequestDelegate 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.
- *
- * Override handleSuccess
to parse the body of the resource
- * request and call the request callback. handleSuccess
is
- * invoked via the executor, so you don't need to delegate further.
- */
- protected abstract class ResourceDelegate extends BaseResourceDelegate {
- protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
-
- protected final RequestDelegate 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 delegate) {
- this(resource, delegate, null, null, false);
- }
-
- /**
- * Create a delegate for a Hawk-authenticated resource.
- */
- public ResourceDelegate(final Resource resource, final RequestDelegate 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 void post(BaseResource resource, final JSONObject requestBody, final RequestDelegate 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 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 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(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 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(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 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(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 delegate) {
- login(new FxAccount10AuthDelegate(email, stretchedPWBytes), delegate);
- }
-
- protected void login(final AuthDelegate authDelegate, final RequestDelegate delegate) {
- authStart(authDelegate, new RequestDelegate() {
- @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 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(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 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(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 unbundleBody
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 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(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.
- *
- * 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 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(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 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(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 requestDelegate);
+ public void status(byte[] sessionToken, RequestDelegate requestDelegate);
+ public void keys(byte[] keyFetchToken, RequestDelegate requestDelegate);
+ public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate requestDelegate);
}
diff --git a/mobile/android/base/background/fxa/FxAccountClient20.java b/mobile/android/base/background/fxa/FxAccountClient20.java
index 2c24fda2a0a..c805fbfa616 100644
--- a/mobile/android/base/background/fxa/FxAccountClient20.java
+++ b/mobile/android/base/background/fxa/FxAccountClient20.java
@@ -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 };
diff --git a/mobile/android/base/fxa/FxAccountConstants.java.in b/mobile/android/base/fxa/FxAccountConstants.java.in
index 41173353231..bd7cbd6d1b9 100644
--- a/mobile/android/base/fxa/FxAccountConstants.java.in
+++ b/mobile/android/base/fxa/FxAccountConstants.java.in
@@ -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);
+ }
+ }
}
diff --git a/mobile/android/base/fxa/authenticator/AbstractFxAccount.java b/mobile/android/base/fxa/authenticator/AbstractFxAccount.java
index e9abcbe39be..e2c06fb5406 100644
--- a/mobile/android/base/fxa/authenticator/AbstractFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AbstractFxAccount.java
@@ -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();
}
diff --git a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
index 619d53f0adf..88a7c899cd0 100644
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
@@ -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.
*
@@ -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());
+ }
+
+ /**
+ * For debugging only!
+ */
+ public void dump() {
+ if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+ return;
+ }
+ ExtendedJSONObject o = toJSONObject();
+ ArrayList list = new ArrayList(o.keySet());
+ Collections.sort(list);
+ for (String key : list) {
+ FxAccountConstants.pii(LOG_TAG, key + ": " + o.getString(key));
+ }
+ }
+
+ /**
+ * For debugging only!
+ */
+ public void resetAccountTokens() {
+ accountManager.setUserData(account, ACCOUNT_KEY_SESSION_TOKEN, null);
+ accountManager.setUserData(account, ACCOUNT_KEY_KEY_FETCH_TOKEN, null);
+ }
}
diff --git a/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java b/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java
index 431db429e0d..364b3983aa6 100644
--- a/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java
+++ b/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java
@@ -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 getStages(AccountState state) {
+ final LinkedList stages = new LinkedList();
+ 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 stages = new LinkedList();
- 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 stages = getStages(initialState);
+ final LinkedList stageNames = new LinkedList();
+ 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 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 stages
and execute it.
- *
- * This trades stack efficiency for implementation simplicity.
- *
- * @param delegate
- * @param stages
- */
- protected void advance(final String audience, final LinkedList 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 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() {
+ delegate.client.loginAndGetKeys(emailUTF8, quickStretchedPW, new RequestDelegate() {
@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() {
+ @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() {
+ @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() {
+ // 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() {
@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() {
+ delegate.client.sign(sessionToken, publicKey.toJSONObject(), getCertificateDurationInMilliseconds(), new RequestDelegate() {
@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."));
+ }
+ }
}
diff --git a/mobile/android/base/fxa/sync/FxAccountGlobalSession.java b/mobile/android/base/fxa/sync/FxAccountGlobalSession.java
index 5fb7850674f..d75c544a2ac 100644
--- a/mobile/android/base/fxa/sync/FxAccountGlobalSession.java
+++ b/mobile/android/base/fxa/sync/FxAccountGlobalSession.java
@@ -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
diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
index 63d3ac1c977..532867dc690 100644
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -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 list = new ArrayList(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);
+ }
+ }
}
diff --git a/mobile/android/base/resources/values/fxaccount_styles.xml b/mobile/android/base/resources/values/fxaccount_styles.xml
deleted file mode 100644
index c73f4bee04e..00000000000
--- a/mobile/android/base/resources/values/fxaccount_styles.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-