Bug 956844 - Part 2: rework FxA state machine. r=rnewman

This commit is contained in:
Richard Newman 2014-01-16 18:35:08 -08:00
parent c7ef364ed6
commit aebca911a0
10 changed files with 590 additions and 823 deletions

View File

@ -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',

View File

@ -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);
}

View File

@ -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 };

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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."));
}
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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>