Bug 956839 - Implement Android native Firefox Account status UI. r=rnewman

This commit is contained in:
Nick Alexander 2014-01-23 13:54:24 -08:00
parent af02f29c86
commit 2739a12677
32 changed files with 1439 additions and 1152 deletions

View File

@ -508,6 +508,7 @@ sync_java_files = [
'background/fxa/FxAccountClient10.java',
'background/fxa/FxAccountClient20.java',
'background/fxa/FxAccountClientException.java',
'background/fxa/FxAccountRemoteError.java',
'background/fxa/FxAccountUtils.java',
'background/fxa/SkewHandler.java',
'background/healthreport/Environment.java',
@ -555,13 +556,23 @@ sync_java_files = [
'fxa/activities/FxAccountStatusActivity.java',
'fxa/activities/FxAccountUpdateCredentialsActivity.java',
'fxa/activities/FxAccountVerifiedAccountActivity.java',
'fxa/authenticator/AbstractFxAccount.java',
'fxa/authenticator/AndroidFxAccount.java',
'fxa/authenticator/FxAccountAuthenticator.java',
'fxa/authenticator/FxAccountAuthenticatorService.java',
'fxa/authenticator/FxAccountLoginDelegate.java',
'fxa/authenticator/FxAccountLoginException.java',
'fxa/authenticator/FxAccountLoginPolicy.java',
'fxa/login/BaseRequestDelegate.java',
'fxa/login/Cohabiting.java',
'fxa/login/Doghouse.java',
'fxa/login/Engaged.java',
'fxa/login/FxAccountLoginStateMachine.java',
'fxa/login/FxAccountLoginTransition.java',
'fxa/login/Married.java',
'fxa/login/Promised.java',
'fxa/login/Separated.java',
'fxa/login/State.java',
'fxa/login/StateFactory.java',
'fxa/login/TokensAndKeysState.java',
'fxa/sync/FxAccountGlobalSession.java',
'fxa/sync/FxAccountSyncAdapter.java',
'fxa/sync/FxAccountSyncService.java',

View File

@ -17,6 +17,8 @@ import java.util.concurrent.Executor;
import javax.crypto.Mac;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.HKDF;
@ -26,6 +28,7 @@ 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 org.mozilla.gecko.sync.net.SyncStorageResponse;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
@ -57,6 +60,15 @@ public class FxAccountClient10 {
public static final String JSON_KEY_SESSIONTOKEN = "sessionToken";
public static final String JSON_KEY_UID = "uid";
public static final String JSON_KEY_VERIFIED = "verified";
public static final String JSON_KEY_ERROR = "error";
public static final String JSON_KEY_MESSAGE = "message";
public static final String JSON_KEY_INFO = "info";
public static final String JSON_KEY_CODE = "code";
public static final String JSON_KEY_ERRNO = "errno";
protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE, JSON_KEY_INFO };
protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
protected final String serverURI;
protected final Executor executor;
@ -78,7 +90,7 @@ public class FxAccountClient10 {
*/
public interface RequestDelegate<T> {
public void handleError(Exception e);
public void handleFailure(int status, HttpResponse response);
public void handleFailure(FxAccountClientRemoteException e);
public void handleSuccess(T result);
}
@ -181,27 +193,24 @@ public class FxAccountClient10 {
@Override
public void handleHttpResponse(HttpResponse response) {
final int status = response.getStatusLine().getStatusCode();
switch (status) {
case 200:
try {
final int status = validateResponse(response);
skewHandler.updateSkew(response, now());
invokeHandleSuccess(status, response);
return;
default:
} catch (FxAccountClientRemoteException e) {
if (!skewHandler.updateSkew(response, now())) {
// If we couldn't update skew, but we got a failure, let's try clearing the skew.
skewHandler.resetSkew();
}
invokeHandleFailure(status, response);
return;
invokeHandleFailure(e);
}
}
protected void invokeHandleFailure(final int status, final HttpResponse response) {
protected void invokeHandleFailure(final FxAccountClientRemoteException e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(status, response);
delegate.handleFailure(e);
}
});
}
@ -254,6 +263,40 @@ public class FxAccountClient10 {
return System.currentTimeMillis();
}
/**
* Intepret a response from the auth server.
* <p>
* Throw an appropriate exception on errors; otherwise, return the response's
* status code.
*
* @return response's HTTP status code.
* @throws FxAccountClientException
*/
public static int validateResponse(HttpResponse response) throws FxAccountClientRemoteException {
final int status = response.getStatusLine().getStatusCode();
if (status == 200) {
return status;
}
int code;
int errno;
String error;
String message;
String info;
try {
ExtendedJSONObject body = new SyncStorageResponse(response).jsonObjectBody();
body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
code = body.getLong(JSON_KEY_CODE).intValue();
errno = body.getLong(JSON_KEY_ERRNO).intValue();
error = body.getString(JSON_KEY_ERROR);
message = body.getString(JSON_KEY_MESSAGE);
info = body.getString(JSON_KEY_INFO);
} catch (Exception e) {
throw new FxAccountClientMalformedResponseException(response);
}
throw new FxAccountClientRemoteException(response, code, errno, error, message, info);
}
public void createAccount(final String email, final byte[] stretchedPWBytes,
final String srpSalt, final String mainSalt,
final RequestDelegate<String> delegate) {
@ -379,11 +422,11 @@ public class FxAccountClient10 {
}
@Override
public void handleFailure(final int status, final HttpResponse response) {
public void handleFailure(final FxAccountClientRemoteException e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(status, response);
delegate.handleFailure(e);
}
});
}
@ -538,8 +581,8 @@ public class FxAccountClient10 {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
byte[] kA = new byte[32];
byte[] wrapkB = new byte[32];
byte[] kA = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
byte[] wrapkB = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB);
delegate.handleSuccess(new TwoKeys(kA, wrapkB));
return;

View File

@ -4,6 +4,15 @@
package org.mozilla.gecko.background.fxa;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.HttpStatus;
/**
* From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
*/
public class FxAccountClientException extends Exception {
private static final long serialVersionUID = 7953459541558266597L;
@ -14,4 +23,63 @@ public class FxAccountClientException extends Exception {
public FxAccountClientException(Exception e) {
super(e);
}
}
public static class FxAccountClientRemoteException extends FxAccountClientException {
private static final long serialVersionUID = 2209313149952001097L;
public final HttpResponse response;
public final long httpStatusCode;
public final long apiErrorNumber;
public final String error;
public final String message;
public final String info;
public FxAccountClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, String info) {
super(new HTTPFailureException(new SyncStorageResponse(response)));
this.response = response;
this.httpStatusCode = httpStatusCode;
this.apiErrorNumber = apiErrorNumber;
this.error = error;
this.message = message;
this.info = info;
}
@Override
public String toString() {
return "" + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message;
}
public boolean isInvalidAuthentication() {
return httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
}
public boolean isAccountAlreadyExists() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
}
public boolean isBadPassword() {
return apiErrorNumber == FxAccountRemoteError.INCORRECT_PASSWORD;
}
public boolean isUnverified() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
}
public boolean isUpgradeRequired() {
return
apiErrorNumber == FxAccountRemoteError.ENDPOINT_IS_NO_LONGER_SUPPORTED ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT ||
apiErrorNumber == FxAccountRemoteError.INCORRECT_API_VERSION_FOR_THIS_ACCOUNT;
}
}
public static class FxAccountClientMalformedResponseException extends FxAccountClientRemoteException {
private static final long serialVersionUID = 2209313149952001098L;
public FxAccountClientMalformedResponseException(HttpResponse response) {
super(response, 0, FxAccountRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", "Response malformed");
}
}
}

View File

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.fxa;
public interface FxAccountRemoteError {
public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
public static final int ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST = 102;
public static final int INCORRECT_PASSWORD = 103;
public static final int ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT = 104;
public static final int INVALID_VERIFICATION_CODE = 105;
public static final int REQUEST_BODY_WAS_NOT_VALID_JSON = 106;
public static final int REQUEST_BODY_CONTAINS_INVALID_PARAMETERS = 107;
public static final int REQUEST_BODY_MISSING_REQUIRED_PARAMETERS = 108;
public static final int INVALID_REQUEST_SIGNATURE = 109;
public static final int INVALID_AUTHENTICATION_TOKEN = 110;
public static final int INVALID_AUTHENTICATION_TIMESTAMP = 111;
public static final int INVALID_AUTHENTICATION_NONCE = 115;
public static final int CONTENT_LENGTH_HEADER_WAS_NOT_PROVIDED = 112;
public static final int REQUEST_BODY_TOO_LARGE = 113;
public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114;
public static final int INVALID_NONCE_IN_REQUEST_SIGNATURE = 115;
public static final int ENDPOINT_IS_NO_LONGER_SUPPORTED = 116;
public static final int INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT = 117;
public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118;
public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119;
public static final int SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD = 201;
public static final int UNKNOWN_ERROR = 999;
}

View File

@ -22,6 +22,9 @@ public class FxAccountUtils {
public static final int HASH_LENGTH_BYTES = 16;
public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES;
public static final int CRYPTO_KEY_LENGTH_BYTES = 32;
public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES;
public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/";
public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000;
@ -119,4 +122,21 @@ public class FxAccountUtils {
public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32);
}
public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) {
if (unwrapkB == null) {
throw new IllegalArgumentException("unwrapkB must not be null");
}
if (wrapkB == null) {
throw new IllegalArgumentException("wrapkB must not be null");
}
if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) {
throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long");
}
byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES];
for (int i = 0; i < wrapkB.length; i++) {
kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]);
}
return kB;
}
}

View File

@ -130,25 +130,49 @@ public class JSONWebTokenUtils {
/**
* For debugging only!
*
* @param input certificate to dump.
* @return true if the certificate is well-formed.
* @param input
* certificate to dump.
* @return non-null object with keys header, payload, signature if the
* certificate is well-formed.
*/
public static boolean dumpCertificate(String input) {
public static ExtendedJSONObject parseCertificate(String input) {
try {
String[] parts = input.split("\\.");
if (parts.length != 3) {
throw new IllegalArgumentException("certificate must have three parts");
return null;
}
String cHeader = new String(Base64.decodeBase64(parts[0]));
String cPayload = new String(Base64.decodeBase64(parts[1]));
String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
System.out.println("certificate header: " + cHeader);
System.out.println("certificate payload: " + cPayload);
System.out.println("certificate signature: " + cSignature);
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("header", new ExtendedJSONObject(cHeader));
o.put("payload", new ExtendedJSONObject(cPayload));
o.put("signature", cSignature);
return o;
} catch (Exception e) {
return null;
}
}
/**
* For debugging only!
*
* @param input certificate to dump.
* @return true if the certificate is well-formed.
*/
public static boolean dumpCertificate(String input) {
ExtendedJSONObject c = parseCertificate(input);
try {
if (c == null) {
System.out.println("Malformed certificate -- got exception trying to dump contents.");
return false;
}
System.out.println("certificate header: " + c.getString("header"));
System.out.println("certificate payload: " + c.getString("payload"));
System.out.println("certificate signature: " + c.getString("signature"));
return true;
} catch (Exception e) {
System.out.println("Malformed certificate -- got exception trying to dump contents.");
e.printStackTrace();
return false;
}
}
@ -159,31 +183,54 @@ public class JSONWebTokenUtils {
* @param input assertion to dump.
* @return true if the assertion is well-formed.
*/
public static boolean dumpAssertion(String input) {
public static ExtendedJSONObject parseAssertion(String input) {
try {
String[] parts = input.split("~");
if (parts.length != 2) {
throw new IllegalArgumentException("input must have two parts");
return null;
}
String certificate = parts[0];
String assertion = parts[1];
parts = assertion.split("\\.");
if (parts.length != 3) {
throw new IllegalArgumentException("assertion must have three parts");
return null;
}
String aHeader = new String(Base64.decodeBase64(parts[0]));
String aPayload = new String(Base64.decodeBase64(parts[1]));
String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
// We do all the assertion parsing *before* dumping the certificate in
// case there's a malformed assertion.
dumpCertificate(certificate);
System.out.println("assertion header: " + aHeader);
System.out.println("assertion payload: " + aPayload);
System.out.println("assertion signature: " + aSignature);
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("header", new ExtendedJSONObject(aHeader));
o.put("payload", new ExtendedJSONObject(aPayload));
o.put("signature", aSignature);
o.put("certificate", certificate);
return o;
} catch (Exception e) {
return null;
}
}
/**
* For debugging only!
*
* @param input assertion to dump.
* @return true if the assertion is well-formed.
*/
public static boolean dumpAssertion(String input) {
ExtendedJSONObject a = parseAssertion(input);
try {
if (a == null) {
System.out.println("Malformed assertion -- got exception trying to dump contents.");
return false;
}
dumpCertificate(a.getString("certificate"));
System.out.println("assertion header: " + a.getString("header"));
System.out.println("assertion payload: " + a.getString("payload"));
System.out.println("assertion signature: " + a.getString("signature"));
return true;
} catch (Exception e) {
System.out.println("Malformed assertion -- got exception trying to dump contents.");
e.printStackTrace();
return false;
}
}

View File

@ -11,9 +11,8 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import android.app.Activity;
import android.content.Context;
@ -22,7 +21,6 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import android.widget.Toast;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* Activity which displays account created successfully screen to the user, and
@ -114,8 +112,8 @@ public class FxAccountConfirmAccountActivity extends Activity implements OnClick
}
@Override
public void handleFailure(int status, HttpResponse response) {
handleError(new HTTPFailureException(new SyncStorageResponse(response)));
public void handleFailure(FxAccountClientRemoteException e) {
handleError(e);
}
@Override

View File

@ -12,14 +12,15 @@ import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccountTask;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.fxa.login.Promised;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlertDialog;
@ -33,7 +34,6 @@ import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* Activity which displays create account screen to the user.
@ -149,27 +149,31 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
}
@Override
public void handleFailure(int status, HttpResponse response) {
handleError(new HTTPFailureException(new SyncStorageResponse(response)));
public void handleFailure(final FxAccountClientRemoteException e) {
handleError(e);
}
@Override
public void handleSuccess(String result) {
public void handleSuccess(String uid) {
Activity activity = FxAccountCreateAccountActivity.this;
Logger.info(LOG_TAG, "Got success creating account.");
// We're on the UI thread, but it's okay to create the account here.
Account account;
AndroidFxAccount fxAccount;
try {
final String profile = Constants.DEFAULT_PROFILE;
final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_URI;
account = AndroidFxAccount.addAndroidAccount(activity, email, password,
// TODO: This is wasteful. We should be able to thread these through so they don't get recomputed.
byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(email.getBytes("UTF-8"), password.getBytes("UTF-8"));
byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
State state = new Promised(email, uid, false, unwrapkB, quickStretchedPW);
fxAccount = AndroidFxAccount.addAndroidAccount(activity, email, password,
profile,
serverURI,
tokenServerURI,
null, null, false);
if (account == null) {
throw new RuntimeException("XXX what?");
state);
if (fxAccount == null) {
throw new RuntimeException("Could not add Android account.");
}
} catch (Exception e) {
handleError(e);
@ -178,7 +182,7 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
new AndroidFxAccount(activity, account).dump();
fxAccount.dump();
}
// The GetStarted activity has called us and needs to return a result to the authenticator.

View File

@ -12,15 +12,13 @@ import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.InnerRequestDelegate;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* An <code>AsyncTask</code> wrapper around signing up for, and signing in to, a
@ -75,10 +73,7 @@ abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestD
}
// We are on the UI thread, and need to invoke these callbacks here to allow UI updating.
if (result.exception instanceof HTTPFailureException) {
HTTPFailureException e = (HTTPFailureException) result.exception;
delegate.handleFailure(e.response.getStatusCode(), e.response.httpResponse());
} else if (innerDelegate.exception != null) {
if (innerDelegate.exception != null) {
delegate.handleError(innerDelegate.exception);
} else {
delegate.handleSuccess(result.response);
@ -110,9 +105,9 @@ abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestD
}
@Override
public void handleFailure(int status, HttpResponse response) {
public void handleFailure(FxAccountClientRemoteException e) {
Logger.warn(LOG_TAG, "Got failure.");
this.exception = new HTTPFailureException(new SyncStorageResponse(response));
this.exception = e;
latch.countDown();
}

View File

@ -12,14 +12,15 @@ import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Intent;
@ -29,7 +30,6 @@ import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* Activity which displays sign in screen to the user.
@ -118,8 +118,8 @@ public class FxAccountSignInActivity extends FxAccountAbstractSetupActivity {
}
@Override
public void handleFailure(int status, HttpResponse response) {
showRemoteError(new HTTPFailureException(new SyncStorageResponse(response)));
public void handleFailure(FxAccountClientRemoteException e) {
showRemoteError(e);
}
@Override
@ -128,16 +128,21 @@ public class FxAccountSignInActivity extends FxAccountAbstractSetupActivity {
Logger.info(LOG_TAG, "Got success signing in.");
// We're on the UI thread, but it's okay to create the account here.
Account account;
AndroidFxAccount fxAccount;
try {
final String profile = Constants.DEFAULT_PROFILE;
final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_URI;
account = AndroidFxAccount.addAndroidAccount(activity, email, password,
// TODO: This is wasteful. We should be able to thread these through so they don't get recomputed.
byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(email.getBytes("UTF-8"), password.getBytes("UTF-8"));
byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
State state = new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken);
fxAccount = AndroidFxAccount.addAndroidAccount(activity, email, password,
profile,
serverURI,
tokenServerURI,
profile, result.sessionToken, result.keyFetchToken, result.verified);
if (account == null) {
throw new RuntimeException("XXX what?");
state);
if (fxAccount == null) {
throw new RuntimeException("Could not add Android account.");
}
} catch (Exception e) {
handleError(e);
@ -146,7 +151,7 @@ public class FxAccountSignInActivity extends FxAccountAbstractSetupActivity {
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
new AndroidFxAccount(activity, account).dump();
fxAccount.dump();
}
// The GetStarted activity has called us and needs to return a result to the authenticator.

View File

@ -6,13 +6,18 @@ package org.mozilla.gecko.fxa.activities;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import android.accounts.Account;
import android.content.ContentResolver;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
/**
@ -24,6 +29,7 @@ public class FxAccountStatusActivity extends FxAccountAbstractActivity {
protected View connectionStatusUnverifiedView;
protected View connectionStatusSignInView;
protected View connectionStatusSyncingView;
protected TextView emailTextView;
public FxAccountStatusActivity() {
super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
@ -45,6 +51,107 @@ public class FxAccountStatusActivity extends FxAccountAbstractActivity {
connectionStatusSyncingView = ensureFindViewById(null, R.id.syncing_view, "syncing view");
launchActivityOnClick(connectionStatusSignInView, FxAccountUpdateCredentialsActivity.class);
emailTextView = (TextView) findViewById(R.id.email);
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
createDebugButtons();
}
}
protected void createDebugButtons() {
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
return;
}
findViewById(R.id.debug_buttons).setVisibility(View.VISIBLE);
findViewById(R.id.debug_refresh_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Refreshing.");
refresh();
}
});
findViewById(R.id.debug_dump_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Dumping account details.");
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
if (accounts.length < 1) {
return;
}
AndroidFxAccount account = new AndroidFxAccount(FxAccountStatusActivity.this, accounts[0]);
account.dump();
}
});
findViewById(R.id.debug_sync_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Syncing.");
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
if (accounts.length < 1) {
return;
}
final Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
ContentResolver.requestSync(accounts[0], BrowserContract.AUTHORITY, extras);
// No sense refreshing, since the sync will complete in the future.
}
});
findViewById(R.id.debug_forget_certificate_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
if (accounts.length < 1) {
return;
}
AndroidFxAccount account = new AndroidFxAccount(FxAccountStatusActivity.this, accounts[0]);
State state = account.getState();
try {
Married married = (Married) state;
Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
account.setState(married.makeCohabitingState());
refresh();
} catch (ClassCastException e) {
Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
// Ignore.
}
}
});
findViewById(R.id.debug_require_password_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
if (accounts.length < 1) {
return;
}
AndroidFxAccount account = new AndroidFxAccount(FxAccountStatusActivity.this, accounts[0]);
State state = account.getState();
account.setState(state.makeSeparatedState());
refresh();
}
});
findViewById(R.id.debug_require_upgrade_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
if (accounts.length < 1) {
return;
}
AndroidFxAccount account = new AndroidFxAccount(FxAccountStatusActivity.this, accounts[0]);
State state = account.getState();
account.setState(state.makeDoghouseState());
refresh();
}
});
}
@Override
@ -53,36 +160,55 @@ public class FxAccountStatusActivity extends FxAccountAbstractActivity {
refresh();
}
protected void refresh(Account account) {
TextView email = (TextView) findViewById(R.id.email);
protected void showNeedsUpgrade() {
connectionStatusUnverifiedView.setVisibility(View.GONE);
connectionStatusSignInView.setVisibility(View.GONE);
connectionStatusSyncingView.setVisibility(View.GONE);
}
protected void showNeedsVerification() {
connectionStatusUnverifiedView.setVisibility(View.VISIBLE);
connectionStatusSignInView.setVisibility(View.GONE);
connectionStatusSyncingView.setVisibility(View.GONE);
}
protected void showNeedsPassword() {
connectionStatusUnverifiedView.setVisibility(View.GONE);
connectionStatusSignInView.setVisibility(View.VISIBLE);
connectionStatusSyncingView.setVisibility(View.GONE);
return;
}
protected void showConnected() {
connectionStatusUnverifiedView.setVisibility(View.GONE);
connectionStatusSignInView.setVisibility(View.GONE);
connectionStatusSyncingView.setVisibility(View.VISIBLE);
return;
}
protected void refresh(Account account) {
if (account == null) {
redirectToActivity(FxAccountGetStartedActivity.class);
return;
}
emailTextView.setText(account.name);
// Interrogate the Firefox Account's state.
AndroidFxAccount fxAccount = new AndroidFxAccount(this, account);
email.setText(account.name);
// Not as good as interrogating state machine, but will do for now.
if (!fxAccount.isVerified()) {
connectionStatusUnverifiedView.setVisibility(View.VISIBLE);
connectionStatusSignInView.setVisibility(View.GONE);
connectionStatusSyncingView.setVisibility(View.GONE);
return;
State state = fxAccount.getState();
switch (state.getNeededAction()) {
case NeedsUpgrade:
showNeedsUpgrade();
break;
case NeedsPassword:
showNeedsPassword();
break;
case NeedsVerification:
showNeedsVerification();
break;
default:
showConnected();
}
if (fxAccount.getQuickStretchedPW() == null) {
connectionStatusUnverifiedView.setVisibility(View.GONE);
connectionStatusSignInView.setVisibility(View.VISIBLE);
connectionStatusSyncingView.setVisibility(View.GONE);
return;
}
connectionStatusUnverifiedView.setVisibility(View.GONE);
connectionStatusSignInView.setVisibility(View.GONE);
connectionStatusSyncingView.setVisibility(View.VISIBLE);
}
protected void refresh() {
@ -93,66 +219,4 @@ public class FxAccountStatusActivity extends FxAccountAbstractActivity {
}
refresh(accounts[0]);
}
protected void dumpAccountDetails() {
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
if (accounts.length < 1) {
return;
}
AndroidFxAccount fxAccount = new AndroidFxAccount(this, accounts[0]);
fxAccount.dump();
}
protected void forgetAccountTokens() {
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
if (accounts.length < 1) {
return;
}
AndroidFxAccount fxAccount = new AndroidFxAccount(this, accounts[0]);
fxAccount.forgetAccountTokens();
fxAccount.dump();
}
protected void forgetQuickStretchedPW() {
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
if (accounts.length < 1) {
return;
}
AndroidFxAccount fxAccount = new AndroidFxAccount(this, accounts[0]);
fxAccount.forgetQuickstretchedPW();
fxAccount.dump();
}
public void onClickRefresh(View view) {
Logger.debug(LOG_TAG, "Refreshing.");
refresh();
}
public void onClickForgetAccountTokens(View view) {
Logger.debug(LOG_TAG, "Forgetting account tokens.");
forgetAccountTokens();
}
public void onClickForgetPassword(View view) {
Logger.debug(LOG_TAG, "Forgetting quickStretchedPW.");
forgetQuickStretchedPW();
}
public void onClickDumpAccountDetails(View view) {
Logger.debug(LOG_TAG, "Dumping account details.");
dumpAccountDetails();
}
public void onClickGetStarted(View view) {
Logger.debug(LOG_TAG, "Launching get started activity.");
redirectToActivity(FxAccountGetStartedActivity.class);
}
public void onClickVerify(View view) {
Logger.debug(LOG_TAG, "Launching verification activity.");
}
public void onClickSignIn(View view) {
Logger.debug(LOG_TAG, "Launching sign in again activity.");
}
}

View File

@ -14,23 +14,24 @@ import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.Separated;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import android.accounts.Account;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import ch.boye.httpclientandroidlib.HttpResponse;
/**
* Activity which displays a screen for updating the local password.
@ -38,7 +39,8 @@ import ch.boye.httpclientandroidlib.HttpResponse;
public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupActivity {
protected static final String LOG_TAG = FxAccountUpdateCredentialsActivity.class.getSimpleName();
protected Account account;
protected AndroidFxAccount fxAccount;
protected Separated accountState;
public FxAccountUpdateCredentialsActivity() {
// We want to share code with the other setup activities, but this activity
@ -80,13 +82,28 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
public void onResume() {
super.onResume();
Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
account = accounts[0];
if (account == null) {
if (accounts.length < 1 || accounts[0] == null) {
Logger.warn(LOG_TAG, "No Android accounts.");
setResult(RESULT_CANCELED);
finish();
return;
}
emailEdit.setText(account.name);
this.fxAccount = new AndroidFxAccount(this, accounts[0]);
if (fxAccount == null) {
Logger.warn(LOG_TAG, "Could not get Firefox Account from Android account.");
setResult(RESULT_CANCELED);
finish();
return;
}
State state = fxAccount.getState();
if (state.getStateLabel() != StateLabel.Separated) {
Logger.warn(LOG_TAG, "Could not get state from Firefox Account.");
setResult(RESULT_CANCELED);
finish();
return;
}
this.accountState = (Separated) state;
emailEdit.setText(fxAccount.getAndroidAccount().name);
}
protected class UpdateCredentialsDelegate implements RequestDelegate<LoginResponse> {
@ -99,6 +116,7 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
this.email = email;
this.password = password;
this.serverURI = serverURI;
// XXX This needs to be calculated lazily.
this.quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(email.getBytes("UTF-8"), password.getBytes("UTF-8"));
}
@ -108,23 +126,28 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
}
@Override
public void handleFailure(int status, HttpResponse response) {
showRemoteError(new HTTPFailureException(new SyncStorageResponse(response)));
public void handleFailure(FxAccountClientRemoteException e) {
// TODO On isUpgradeRequired, transition to Doghouse state.
showRemoteError(e);
}
@Override
public void handleSuccess(LoginResponse result) {
Activity activity = FxAccountUpdateCredentialsActivity.this;
Logger.info(LOG_TAG, "Got success signing in.");
if (account == null) {
Logger.warn(LOG_TAG, "account must not be null");
if (fxAccount == null) {
this.handleError(new IllegalStateException("fxAccount must not be null"));
return;
}
AndroidFxAccount fxAccount = new AndroidFxAccount(activity, account);
// XXX wasteful, should only do this once.
fxAccount.setQuickStretchedPW(quickStretchedPW);
byte[] unwrapkB;
try {
unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
} catch (Exception e) {
this.handleError(e);
return;
}
fxAccount.setState(new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken));
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {

View File

@ -1,99 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.authenticator;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
/**
* A representation of a Firefox Account.
* <p>
* Keeps track of:
* <ul>
* <li>tokens;</li>
* <li>verification state;</li>
* <li>auth server managed keys;</li>
* <li>locally managed key pairs</li>
* </ul>
* <p>
* <code>kA</code> is a <i>recoverable</i> auth server managed key.
* <code>kB</code> is an <i>unrecoverable</i> auth server managed key. Changing
* the user's password maintains <code>kA</code> and <code>kB</code>, but
* resetting the user's password retains only <code>kA</code> (and losees
* <code>kB</code>).
* <p>
* The entropy of <code>kB</code> is partially derived from the server and
* partially from the user's password. The auth server stores <code>kB</code>
* remotely, wrapped in a key derived from the user's password. The unwrapping
* process is implementation specific, but it is expected that the appropriate
* derivative of the user's password will be stored until
* <code>setWrappedKb</code> is called, at which point <code>kB</code> will be
* computed and cached, ready to be returned by <code>getKb</code>.
*/
public interface AbstractFxAccount {
/**
* Get the Firefox Account auth server URI that this account login flow should
* talk to.
*/
public String getAccountServerURI();
/**
* @return the profile name associated with the account, such as "default".
*/
public String getProfile();
public boolean isValid();
public void setInvalid();
public byte[] getSessionToken();
public byte[] getKeyFetchToken();
public void setSessionToken(byte[] token);
public void setKeyFetchToken(byte[] token);
/**
* Return true if and only if this account is guaranteed to be verified. This
* is intended to be a local cache of the verified state. Do not query the
* auth server!
*/
public boolean isVerified();
/**
* Update the account's local cache to reflect that this account is known to
* be verified.
*/
public void setVerified();
public byte[] getKa();
public void setKa(byte[] kA);
public byte[] getKb();
/**
* The auth server returns <code>kA</code> and <code>wrap(kB)</code> in
* response to <code>/account/keys</code>. This method accepts that wrapped
* value and uses whatever (per concrete type) method it can to derive the
* unwrapped value and cache it for retrieval by <code>getKb</code>.
* <p>
* See also {@link AbstractFxAccount}.
*
* @param wrappedKb <code>wrap(kB)</code> from auth server response.
*/
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();
public void setQuickStretchedPW(byte[] quickStretchedPW);
}

View File

@ -12,11 +12,10 @@ import java.util.ArrayList;
import java.util.Collections;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.StateFactory;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
@ -33,7 +32,7 @@ import android.os.Bundle;
* Account type. Account user data is not removed when the App's private data is
* cleared.
*/
public class AndroidFxAccount implements AbstractFxAccount {
public class AndroidFxAccount {
protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
public static final int CURRENT_PREFS_VERSION = 1;
@ -48,18 +47,10 @@ public class AndroidFxAccount implements AbstractFxAccount {
public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI"; // Sync-specific.
public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
public static final int CURRENT_BUNDLE_VERSION = 1;
public static final int CURRENT_BUNDLE_VERSION = 2;
public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
public static final String BUNDLE_KEY_ASSERTION = "assertion";
public static final String BUNDLE_KEY_CERTIFICATE = "certificate";
public static final String BUNDLE_KEY_INVALID = "invalid";
public static final String BUNDLE_KEY_SESSION_TOKEN = "sessionToken";
public static final String BUNDLE_KEY_KEY_FETCH_TOKEN = "keyFetchToken";
public static final String BUNDLE_KEY_VERIFIED = "verified";
public static final String BUNDLE_KEY_KA = "kA";
public static final String BUNDLE_KEY_KB = "kB";
public static final String BUNDLE_KEY_UNWRAPKB = "unwrapkB";
public static final String BUNDLE_KEY_ASSERTION_KEY_PAIR = "assertionKeyPair";
public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
public static final String BUNDLE_KEY_STATE = "state";
protected final Context context;
protected final AccountManager accountManager;
@ -87,6 +78,10 @@ public class AndroidFxAccount implements AbstractFxAccount {
this.accountManager = AccountManager.get(this.context);
}
public Account getAndroidAccount() {
return this.account;
}
protected int getAccountVersion() {
String v = accountManager.getUserData(account, ACCOUNT_KEY_ACCOUNT_VERSION);
if (v == null) {
@ -107,7 +102,8 @@ public class AndroidFxAccount implements AbstractFxAccount {
protected ExtendedJSONObject unbundle() {
final int version = getAccountVersion();
if (version < CURRENT_ACCOUNT_VERSION) {
// Needs upgrade. For now, do nothing.
// Needs upgrade. For now, do nothing. We'd like to just put your account
// into the Separated state here and have you update your credentials.
return null;
}
@ -120,7 +116,7 @@ public class AndroidFxAccount implements AbstractFxAccount {
if (bundle == null) {
return null;
}
return unbundleAccountV1(bundle);
return unbundleAccountV2(bundle);
}
protected String getBundleData(String key) {
@ -186,26 +182,18 @@ public class AndroidFxAccount implements AbstractFxAccount {
return null;
}
@Override
public byte[] getEmailUTF8() {
try {
return account.name.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// Ignore.
return null;
}
private ExtendedJSONObject unbundleAccountV2(String bundle) {
return unbundleAccountV1(bundle);
}
/**
* Note that if the user clears data, an account will be left pointing to a
* deleted profile. Such is life.
*/
@Override
public String getProfile() {
return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE);
}
@Override
public String getAccountServerURI() {
return accountManager.getUserData(account, ACCOUNT_KEY_IDP_SERVER);
}
@ -251,128 +239,6 @@ public class AndroidFxAccount implements AbstractFxAccount {
return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
}
@Override
public void setQuickStretchedPW(byte[] quickStretchedPW) {
accountManager.setPassword(account, quickStretchedPW == null ? null : Utils.byte2Hex(quickStretchedPW));
}
@Override
public byte[] getQuickStretchedPW() {
String quickStretchedPW = accountManager.getPassword(account);
return quickStretchedPW == null ? null : Utils.hex2Byte(quickStretchedPW);
}
@Override
public byte[] getSessionToken() {
return getBundleDataBytes(BUNDLE_KEY_SESSION_TOKEN);
}
@Override
public byte[] getKeyFetchToken() {
return getBundleDataBytes(BUNDLE_KEY_KEY_FETCH_TOKEN);
}
@Override
public void setSessionToken(byte[] sessionToken) {
updateBundleDataBytes(BUNDLE_KEY_SESSION_TOKEN, sessionToken);
}
@Override
public void setKeyFetchToken(byte[] keyFetchToken) {
updateBundleDataBytes(BUNDLE_KEY_KEY_FETCH_TOKEN, keyFetchToken);
}
@Override
public boolean isVerified() {
return getBundleDataBoolean(BUNDLE_KEY_VERIFIED, false);
}
@Override
public void setVerified() {
updateBundleValue(BUNDLE_KEY_VERIFIED, true);
}
@Override
public byte[] getKa() {
return getBundleDataBytes(BUNDLE_KEY_KA);
}
@Override
public void setKa(byte[] kA) {
updateBundleValue(BUNDLE_KEY_KA, Utils.byte2Hex(kA));
}
@Override
public void setWrappedKb(byte[] wrappedKb) {
if (wrappedKb == null) {
final String message = "wrappedKb is null: cannot set kB.";
Logger.error(LOG_TAG, message);
throw new IllegalArgumentException(message);
}
byte[] unwrapKb = getBundleDataBytes(BUNDLE_KEY_UNWRAPKB);
if (unwrapKb == null) {
Logger.error(LOG_TAG, "unwrapKb is null: cannot set kB.");
return;
}
byte[] kB = new byte[wrappedKb.length]; // We could hard-code this to be 32.
for (int i = 0; i < wrappedKb.length; i++) {
kB[i] = (byte) (wrappedKb[i] ^ unwrapKb[i]);
}
updateBundleValue(BUNDLE_KEY_KB, Utils.byte2Hex(kB));
}
@Override
public byte[] getKb() {
return getBundleDataBytes(BUNDLE_KEY_KB);
}
protected BrowserIDKeyPair generateNewAssertionKeyPair() throws GeneralSecurityException {
Logger.info(LOG_TAG, "Generating new assertion key pair.");
// TODO Have the key size be a non-constant in FxAccountUtils, or read from SharedPreferences, or...
return RSACryptoImplementation.generateKeyPair(1024);
}
@Override
public BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException {
try {
String data = getBundleData(BUNDLE_KEY_ASSERTION_KEY_PAIR);
return RSACryptoImplementation.fromJSONObject(new ExtendedJSONObject(data));
} catch (Exception e) {
// Fall through to generating a new key pair.
}
BrowserIDKeyPair keyPair = generateNewAssertionKeyPair();
ExtendedJSONObject descriptor = unbundle();
if (descriptor == null) {
descriptor = new ExtendedJSONObject();
}
descriptor.put(BUNDLE_KEY_ASSERTION_KEY_PAIR, keyPair.toJSONObject().toJSONString());
persistBundle(descriptor);
return keyPair;
}
@Override
public String getCertificate() {
return getBundleData(BUNDLE_KEY_CERTIFICATE);
}
@Override
public void setCertificate(String certificate) {
updateBundleValue(BUNDLE_KEY_CERTIFICATE, certificate);
}
@Override
public String getAssertion() {
return getBundleData(BUNDLE_KEY_ASSERTION);
}
@Override
public void setAssertion(String assertion) {
updateBundleValue(BUNDLE_KEY_ASSERTION, assertion);
}
/**
* Extract a JSON dictionary of the string values associated to this account.
* <p>
@ -390,20 +256,17 @@ public class AndroidFxAccount implements AbstractFxAccount {
} catch (UnsupportedEncodingException e) {
// Ignore.
}
o.put("quickStretchedPW", accountManager.getPassword(account));
return o;
}
public static Account addAndroidAccount(
public static AndroidFxAccount addAndroidAccount(
Context context,
String email,
String password,
String profile,
String idpServerURI,
String tokenServerURI,
byte[] sessionToken,
byte[] keyFetchToken,
boolean verified)
State state)
throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
if (email == null) {
throw new IllegalArgumentException("email must not be null");
@ -417,20 +280,10 @@ public class AndroidFxAccount implements AbstractFxAccount {
if (tokenServerURI == null) {
throw new IllegalArgumentException("tokenServerURI must not be null");
}
// sessionToken and keyFetchToken are allowed to be null; they can be
// fetched via /account/login from the password. These tokens are generated
// by the server and we have no length or formatting guarantees. However, if
// one is given, both should be given: they come from the server together.
if ((sessionToken == null && keyFetchToken != null) ||
(sessionToken != null && keyFetchToken == null)) {
throw new IllegalArgumentException("none or both of sessionToken and keyFetchToken may be null");
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
byte[] emailUTF8 = email.getBytes("UTF-8");
byte[] passwordUTF8 = password.getBytes("UTF-8");
byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(emailUTF8, passwordUTF8);
byte[] unwrapBkey = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
// Android has internal restrictions that require all values in this
// bundle to be strings. *sigh*
Bundle userdata = new Bundle();
@ -441,39 +294,71 @@ public class AndroidFxAccount implements AbstractFxAccount {
userdata.putString(ACCOUNT_KEY_PROFILE, profile);
ExtendedJSONObject descriptor = new ExtendedJSONObject();
descriptor.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
descriptor.put(BUNDLE_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken));
descriptor.put(BUNDLE_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken));
descriptor.put(BUNDLE_KEY_VERIFIED, verified);
descriptor.put(BUNDLE_KEY_UNWRAPKB, Utils.byte2Hex(unwrapBkey));
descriptor.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
descriptor.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
descriptor.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
userdata.putString(ACCOUNT_KEY_DESCRIPTOR, descriptor.toJSONString());
Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
AccountManager accountManager = AccountManager.get(context);
boolean added = accountManager.addAccountExplicitly(account, Utils.byte2Hex(quickStretchedPW), userdata);
boolean added = accountManager.addAccountExplicitly(account, null, userdata); // XXX what should the password be?
if (!added) {
return null;
}
AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
fxAccount.clearSyncPrefs();
fxAccount.enableSyncing();
return fxAccount;
}
public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE).edit().clear().commit();
}
public void enableSyncing() {
FxAccountAuthenticator.enableSyncing(context, account);
return account;
}
public void disableSyncing() {
FxAccountAuthenticator.disableSyncing(context, account);
}
public synchronized void setState(State state) {
if (state == null) {
throw new IllegalArgumentException("state must not be null");
}
updateBundleValue(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
updateBundleValue(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
}
public synchronized State getState() {
String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL);
String stateString = getBundleData(BUNDLE_KEY_STATE);
if (stateLabelString == null) {
throw new IllegalStateException("stateLabelString must not be null");
}
if (stateString == null) {
throw new IllegalStateException("stateString must not be null");
}
try {
StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
} catch (Exception e) {
throw new IllegalStateException("could not get state", e);
}
}
// TODO: this is shit.
private static String computeAudience(String tokenServerURI) throws URISyntaxException {
URI uri = new URI(tokenServerURI);
return new URI(uri.getScheme(), uri.getHost(), null, null).toString();
URI uri = new URI(tokenServerURI);
return new URI(uri.getScheme(), uri.getHost(), null, null).toString();
}
@Override
public boolean isValid() {
return !getBundleDataBoolean(BUNDLE_KEY_INVALID, false);
}
@Override
public void setInvalid() {
updateBundleValue(BUNDLE_KEY_INVALID, true);
}
/**
* <b>For debugging only!</b>
@ -489,24 +374,4 @@ public class AndroidFxAccount implements AbstractFxAccount {
FxAccountConstants.pii(LOG_TAG, key + ": " + o.get(key));
}
}
/**
* <b>For debugging only!</b>
*/
public void forgetAccountTokens() {
ExtendedJSONObject descriptor = unbundle();
if (descriptor == null) {
return;
}
descriptor.remove(BUNDLE_KEY_SESSION_TOKEN);
descriptor.remove(BUNDLE_KEY_KEY_FETCH_TOKEN);
persistBundle(descriptor);
}
/**
* <b>For debugging only!</b>
*/
public void forgetQuickstretchedPW() {
accountManager.setPassword(account, null);
}
}

View File

@ -47,6 +47,14 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
}
}
protected static void disableSyncing(Context context, Account account) {
for (String authority : new String[] {
AppConstants.ANDROID_PACKAGE_NAME + ".db.browser",
}) {
ContentResolver.setSyncAutomatically(account, authority, false);
}
}
public static Account addAccount(Context context, String email, String uid, String sessionToken, String kA, String kB) {
final AccountManager accountManager = AccountManager.get(context);
final Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);

View File

@ -1,590 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.authenticator;
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.background.fxa.SkewHandler;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
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;
import ch.boye.httpclientandroidlib.HttpResponse;
public class FxAccountLoginPolicy {
public static final String LOG_TAG = FxAccountLoginPolicy.class.getSimpleName();
public final Context context;
public final AbstractFxAccount fxAccount;
public final Executor executor;
public FxAccountLoginPolicy(Context context, AbstractFxAccount fxAccount, Executor executor) {
this.context = context;
this.fxAccount = fxAccount;
this.executor = executor;
}
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.getAccountServerURI();
return new FxAccountClient20(serverURI, executor);
}
private SkewHandler skewHandler;
/**
* Check if this certificate is not worth generating an assertion from: for
* example, because it is not well-formed, or it is already expired.
*
* @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;
}
protected long now() {
return System.currentTimeMillis();
}
public enum AccountState {
Invalid,
NeedsSessionToken,
NeedsVerification,
NeedsKeys,
NeedsCertificate,
NeedsAssertion,
Valid,
};
public AccountState getAccountState(AbstractFxAccount fxAccount) {
String serverURI = fxAccount.getAccountServerURI();
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;
}
public void login(final String audience, final FxAccountLoginDelegate delegate, final SkewHandler skewHandler) {
this.skewHandler = skewHandler;
this.login(audience, delegate);
}
/**
* Do as much of a Firefox Account login dance as possible.
* <p>
* To avoid deeply nested callbacks, we maintain a simple queue of stages to
* execute in sequence.
*
* @param audience to generate assertion for.
* @param delegate providing callbacks to invoke.
*/
public void login(final String audience, final FxAccountLoginDelegate delegate) {
final AccountState initialState = getAccountState(fxAccount);
Logger.info(LOG_TAG, "Logging in account from initial state " + initialState + ".");
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 class LoginStageDelegate {
public final LinkedList<LoginStage> stages;
public final String audience;
public final FxAccountLoginDelegate delegate;
public final FxAccountClient client;
protected LoginStage currentStage = null;
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;
}
public void handleError(FxAccountLoginException e) {
invokeHandleHardFailure(delegate, e);
}
}
public class EnsureSessionTokenStage implements LoginStage {
@Override
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");
}
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 (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
if (status != 401) {
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 (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
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."));
}
@Override
public void handleSuccess(StatusResponse result) {
// We're not yet verified. We can't go forward yet.
if (!result.verified) {
delegate.handleError(new FxAccountLoginAccountNotVerifiedException("Account is not yet verified."));
return;
}
// We've transitioned to verified state. Make a note of it, and continue past go.
fxAccount.setVerified();
delegate.handleStageSuccess();
}
});
}
}
public static int[] DUMMY = null;
public class EnsureKeyFetchTokenStage implements LoginStage {
@Override
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 (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
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) {
throw new IllegalStateException("keyFetchToken must not be null");
}
// 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));
}
@Override
public void handleFailure(int status, HttpResponse response) {
if (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
if (status != 401) {
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
return;
}
delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected key token while fetching keys."));
}
@Override
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 EnsureCertificateStage implements LoginStage {
@Override
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 VerifyingPublicKey publicKey = keyPair.getPublic();
delegate.client.sign(sessionToken, publicKey.toJSONObject(), getCertificateDurationInMilliseconds(), new RequestDelegate<String>() {
@Override
public void handleError(Exception e) {
delegate.handleError(new FxAccountLoginException(e));
}
@Override
public void handleFailure(int status, HttpResponse response) {
if (skewHandler != null) {
skewHandler.updateSkew(response, now());
}
if (status != 401) {
delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
return;
}
// 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) {
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 {
final long now = System.currentTimeMillis();
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 {
// Hurrah for global state. We want to make the timestamp in the
// generated assertion as close to the timestamp on the consuming server
// as possible. In this case, the audience is the consuming server.
SkewHandler skewHandler = SkewHandler.getSkewHandlerFromEndpointString(delegate.audience);
assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, delegate.audience,
JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, now + skewHandler.getSkewInMillis(), 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

@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.background.fxa.FxAccountClient10;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountNeedsVerification;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
public abstract class BaseRequestDelegate<T> implements FxAccountClient10.RequestDelegate<T> {
protected final ExecuteDelegate delegate;
protected final State state;
public BaseRequestDelegate(State state, ExecuteDelegate delegate) {
this.delegate = delegate;
this.state = state;
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
// Order matters here: we don't want to ignore upgrade required responses
// even if the server tells us something else as well. We don't go directly
// to the Doghouse on upgrade required; we want the user to try to update
// their credentials, and then display UI telling them they need to upgrade.
// Then they go to the Doghouse.
if (e.isUpgradeRequired()) {
delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
return;
}
if (e.isInvalidAuthentication()) {
delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
return;
}
if (e.isUnverified()) {
delegate.handleTransition(new AccountNeedsVerification(), state);
return;
}
delegate.handleTransition(new RemoteError(e), state);
}
@Override
public void handleError(Exception e) {
delegate.handleTransition(new LocalError(e), state);
}
}

View File

@ -0,0 +1,46 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class Cohabiting extends TokensAndKeysState {
private static final String LOG_TAG = Cohabiting.class.getSimpleName();
public Cohabiting(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
super(StateLabel.Cohabiting, email, uid, sessionToken, kA, kB, keyPair);
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.getClient().sign(sessionToken, keyPair.getPublic().toJSONObject(), delegate.getCertificateDurationInMilliseconds(),
new BaseRequestDelegate<String>(this, delegate) {
@Override
public void handleSuccess(String certificate) {
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
try {
FxAccountConstants.pii(LOG_TAG, "Fetched certificate: " + certificate);
ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
if (c != null) {
FxAccountConstants.pii(LOG_TAG, "Header : " + c.getObject("header"));
FxAccountConstants.pii(LOG_TAG, "Payload : " + c.getObject("payload"));
FxAccountConstants.pii(LOG_TAG, "Signature: " + c.getString("signature"));
} else {
FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
}
} catch (Exception e) {
FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
}
}
delegate.handleTransition(new LogMessage("sign succeeded"), new Married(email, uid, sessionToken, kA, kB, keyPair, certificate));
}
});
}
}

View File

@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
public class Doghouse extends State {
public Doghouse(String email, String uid, boolean verified) {
super(StateLabel.Doghouse, email, uid, verified);
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new LogMessage("Upgraded Firefox clients might know what to do here."), this);
}
@Override
public Action getNeededAction() {
return Action.NeedsUpgrade;
}
}

View File

@ -0,0 +1,83 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import java.security.NoSuchAlgorithmException;
import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public class Engaged extends State {
private static final String LOG_TAG = Engaged.class.getSimpleName();
protected final byte[] sessionToken;
protected final byte[] keyFetchToken;
protected final byte[] unwrapkB;
public Engaged(String email, String uid, boolean verified, byte[] unwrapkB, byte[] sessionToken, byte[] keyFetchToken) {
super(StateLabel.Engaged, email, uid, verified);
Utils.throwIfNull(unwrapkB, sessionToken, keyFetchToken);
this.unwrapkB = unwrapkB;
this.sessionToken = sessionToken;
this.keyFetchToken = keyFetchToken;
}
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = super.toJSONObject();
// Fields are non-null by constructor.
o.put("unwrapkB", Utils.byte2Hex(unwrapkB));
o.put("sessionToken", Utils.byte2Hex(sessionToken));
o.put("keyFetchToken", Utils.byte2Hex(keyFetchToken));
return o;
}
@Override
public void execute(final ExecuteDelegate delegate) {
BrowserIDKeyPair theKeyPair;
try {
theKeyPair = delegate.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
delegate.handleTransition(new LocalError(e), new Doghouse(email, uid, verified));
return;
}
final BrowserIDKeyPair keyPair = theKeyPair;
delegate.getClient().keys(keyFetchToken, new BaseRequestDelegate<TwoKeys>(this, delegate) {
@Override
public void handleSuccess(TwoKeys result) {
byte[] kB;
try {
kB = FxAccountUtils.unwrapkB(unwrapkB, 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(kB));
}
} catch (Exception e) {
delegate.handleTransition(new RemoteError(e), new Separated(email, uid, verified));
return;
}
delegate.handleTransition(new LogMessage("keys succeeded"), new Cohabiting(email, uid, sessionToken, result.kA, kB, keyPair));
}
});
}
@Override
public Action getNeededAction() {
if (!verified) {
return Action.NeedsVerification;
}
return Action.None;
}
}

View File

@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.State.StateLabel;
public class FxAccountLoginStateMachine {
public static final String LOG_TAG = FxAccountLoginStateMachine.class.getSimpleName();
public interface LoginStateMachineDelegate {
public FxAccountClient getClient();
public long getCertificateDurationInMilliseconds();
public long getAssertionDurationInMilliseconds();
public void handleTransition(Transition transition, State state);
public void handleFinal(State state);
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException;
}
public static class ExecuteDelegate {
protected final LoginStateMachineDelegate delegate;
protected final StateLabel desiredStateLabel;
// It's as difficult to detect arbitrary cycles as repeated states.
protected final Set<StateLabel> stateLabelsSeen = new HashSet<StateLabel>();
protected ExecuteDelegate(StateLabel initialStateLabel, StateLabel desiredStateLabel, LoginStateMachineDelegate delegate) {
this.delegate = delegate;
this.desiredStateLabel = desiredStateLabel;
this.stateLabelsSeen.add(initialStateLabel);
}
public FxAccountClient getClient() {
return delegate.getClient();
}
public long getCertificateDurationInMilliseconds() {
return delegate.getCertificateDurationInMilliseconds();
}
public long getAssertionDurationInMilliseconds() {
return delegate.getAssertionDurationInMilliseconds();
}
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return delegate.generateKeyPair();
}
public void handleTransition(Transition transition, State state) {
// Always trigger the transition callback.
delegate.handleTransition(transition, state);
// Possibly trigger the final callback. We trigger if we're at our desired
// state, or if we've seen this state before.
StateLabel stateLabel = state.getStateLabel();
if (stateLabel == desiredStateLabel || stateLabelsSeen.contains(stateLabel)) {
delegate.handleFinal(state);
return;
}
// If this wasn't the last state, leave a bread crumb and move on to the
// next state.
stateLabelsSeen.add(stateLabel);
state.execute(this);
}
}
public void advance(State initialState, final StateLabel desiredStateLabel, final LoginStateMachineDelegate delegate) {
if (initialState.getStateLabel() == desiredStateLabel) {
// We're already where we want to be!
delegate.handleFinal(initialState);
return;
}
ExecuteDelegate executeDelegate = new ExecuteDelegate(initialState.getStateLabel(), desiredStateLabel, delegate);
initialState.execute(executeDelegate);
}
}

View File

@ -0,0 +1,62 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
public class FxAccountLoginTransition {
public interface Transition {
}
public static class LogMessage implements Transition {
public final String detailMessage;
public LogMessage(String detailMessage) {
this.detailMessage = detailMessage;
}
@Override
public String toString() {
return getClass().getSimpleName() + (this.detailMessage == null ? "" : "('" + this.detailMessage + "')");
}
}
public static class AccountNeedsVerification extends LogMessage {
public AccountNeedsVerification() {
super(null);
}
}
public static class PasswordRequired extends LogMessage {
public PasswordRequired() {
super(null);
}
}
public static class LocalError implements Transition {
public final Exception e;
public LocalError(Exception e) {
this.e = e;
}
@Override
public String toString() {
return "Log(" + this.e + ")";
}
}
public static class RemoteError implements Transition {
public final Exception e;
public RemoteError(Exception e) {
this.e = e;
}
@Override
public String toString() {
return "Log(" + (this.e == null ? "null" : this.e) + ")";
}
}
}

View File

@ -0,0 +1,102 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.KeyBundle;
public class Married extends TokensAndKeysState {
private static final String LOG_TAG = Married.class.getSimpleName();
protected final String certificate;
public Married(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair, String certificate) {
super(StateLabel.Married, email, uid, sessionToken, kA, kB, keyPair);
Utils.throwIfNull(certificate);
this.certificate = certificate;
}
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = super.toJSONObject();
// Fields are non-null by constructor.
o.put("certificate", certificate);
return o;
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new LogMessage("staying married"), this);
}
public String generateAssertion(String audience, String issuer, long issuedAt, long durationInMilliseconds) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException {
String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, issuedAt, durationInMilliseconds);
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
return assertion;
}
try {
FxAccountConstants.pii(LOG_TAG, "Generated assertion: " + assertion);
ExtendedJSONObject a = JSONWebTokenUtils.parseAssertion(assertion);
if (a != null) {
FxAccountConstants.pii(LOG_TAG, "aHeader : " + a.getObject("header"));
FxAccountConstants.pii(LOG_TAG, "aPayload : " + a.getObject("payload"));
FxAccountConstants.pii(LOG_TAG, "aSignature: " + a.getString("signature"));
String certificate = a.getString("certificate");
if (certificate != null) {
ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
FxAccountConstants.pii(LOG_TAG, "cHeader : " + c.getObject("header"));
FxAccountConstants.pii(LOG_TAG, "cPayload : " + c.getObject("payload"));
FxAccountConstants.pii(LOG_TAG, "cSignature: " + c.getString("signature"));
// Print the relevant timestamps in sorted order with labels.
HashMap<Long, String> map = new HashMap<Long, String>();
map.put(a.getObject("payload").getLong("iat"), "aiat");
map.put(a.getObject("payload").getLong("exp"), "aexp");
map.put(c.getObject("payload").getLong("iat"), "ciat");
map.put(c.getObject("payload").getLong("exp"), "cexp");
ArrayList<Long> values = new ArrayList<Long>(map.keySet());
Collections.sort(values);
for (Long value : values) {
FxAccountConstants.pii(LOG_TAG, map.get(value) + ": " + value);
}
} else {
FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
}
} else {
FxAccountConstants.pii(LOG_TAG, "Could not parse assertion!");
}
} catch (Exception e) {
FxAccountConstants.pii(LOG_TAG, "Got exception dumping assertion debug info.");
}
return assertion;
}
public KeyBundle getSyncKeyBundle() throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
// TODO Document this choice for deriving from kB.
return FxAccountUtils.generateSyncKeyBundle(kB);
}
public State makeCohabitingState() {
return new Cohabiting(email, uid, sessionToken, kA, kB, keyPair);
}
}

View File

@ -0,0 +1,59 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import java.io.UnsupportedEncodingException;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public class Promised extends State {
protected final byte[] quickStretchedPW;
protected final byte[] unwrapkB;
public Promised(String email, String uid, boolean verified, byte[] unwrapkB, byte[] quickStretchedPW) {
super(StateLabel.Promised, email, uid, verified);
Utils.throwIfNull(email, uid, unwrapkB, quickStretchedPW);
this.unwrapkB = unwrapkB;
this.quickStretchedPW = quickStretchedPW;
}
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = super.toJSONObject();
// Fields are non-null by constructor.
o.put("unwrapkB", Utils.byte2Hex(unwrapkB));
o.put("quickStretchedPW", Utils.byte2Hex(quickStretchedPW));
return o;
}
@Override
public void execute(final ExecuteDelegate delegate) {
byte[] emailUTF8;
try {
emailUTF8 = email.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
delegate.handleTransition(new LocalError(e), new Doghouse(email, uid, verified));
return;
}
delegate.getClient().loginAndGetKeys(emailUTF8, quickStretchedPW, new BaseRequestDelegate<FxAccountClient20.LoginResponse>(this, delegate) {
@Override
public void handleSuccess(LoginResponse result) {
delegate.handleTransition(new LogMessage("loginAndGetKeys succeeded"), new Engaged(email, uid, verified, unwrapkB, result.sessionToken, result.keyFetchToken));
}
});
}
@Override
public Action getNeededAction() {
return Action.NeedsVerification;
}
}

View File

@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired;
public class Separated extends State {
public Separated(String email, String uid, boolean verified) {
super(StateLabel.Separated, email, uid, verified);
}
@Override
public void execute(final ExecuteDelegate delegate) {
delegate.handleTransition(new PasswordRequired(), this);
}
@Override
public Action getNeededAction() {
return Action.NeedsPassword;
}
}

View File

@ -0,0 +1,71 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public abstract class State {
public static final long CURRENT_VERSION = 1L;
public enum StateLabel {
Promised,
Engaged,
Cohabiting,
Married,
Separated,
Doghouse,
}
public enum Action {
NeedsUpgrade,
NeedsPassword,
NeedsVerification,
None,
}
protected final StateLabel stateLabel;
public final String email;
public final String uid;
public final boolean verified;
public State(StateLabel stateLabel, String email, String uid, boolean verified) {
Utils.throwIfNull(email, uid);
this.stateLabel = stateLabel;
this.email = email;
this.uid = uid;
this.verified = verified;
}
public StateLabel getStateLabel() {
return this.stateLabel;
}
public boolean isVerified() {
return this.verified;
}
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = new ExtendedJSONObject();
o.put("version", State.CURRENT_VERSION);
o.put("email", email);
o.put("uid", uid);
o.put("verified", verified);
return o;
}
public State makeSeparatedState() {
return new Separated(email, uid, verified);
}
public State makeDoghouseState() {
return new Doghouse(email, uid, verified);
}
public abstract void execute(ExecuteDelegate delegate);
public abstract Action getNeededAction();
}

View File

@ -0,0 +1,69 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
public class StateFactory {
public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
Long version = o.getLong("version");
if (version == null || version.intValue() != 1) {
throw new IllegalStateException("version must be 1");
}
switch (stateLabel) {
case Promised:
return new Promised(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"),
Utils.hex2Byte(o.getString("unwrapkB")),
Utils.hex2Byte(o.getString("quickStretchedPW")));
case Engaged:
return new Engaged(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"),
Utils.hex2Byte(o.getString("unwrapkB")),
Utils.hex2Byte(o.getString("sessionToken")),
Utils.hex2Byte(o.getString("keyFetchToken")));
case Cohabiting:
return new Cohabiting(
o.getString("email"),
o.getString("uid"),
Utils.hex2Byte(o.getString("sessionToken")),
Utils.hex2Byte(o.getString("kA")),
Utils.hex2Byte(o.getString("kB")),
RSACryptoImplementation.fromJSONObject(o.getObject("keyPair")));
case Married:
return new Married(
o.getString("email"),
o.getString("uid"),
Utils.hex2Byte(o.getString("sessionToken")),
Utils.hex2Byte(o.getString("kA")),
Utils.hex2Byte(o.getString("kB")),
RSACryptoImplementation.fromJSONObject(o.getObject("keyPair")),
o.getString("certificate"));
case Separated:
return new Separated(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"));
case Doghouse:
return new Doghouse(
o.getString("email"),
o.getString("uid"),
o.getBoolean("verified"));
default:
throw new IllegalStateException("unrecognized state label: " + stateLabel);
}
}
}

View File

@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
public abstract class TokensAndKeysState extends State {
protected final byte[] sessionToken;
protected final byte[] kA;
protected final byte[] kB;
protected final BrowserIDKeyPair keyPair;
public TokensAndKeysState(StateLabel stateLabel, String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
super(stateLabel, email, uid, true);
Utils.throwIfNull(sessionToken, kA, kB, keyPair);
this.sessionToken = sessionToken;
this.kA = kA;
this.kB = kB;
this.keyPair = keyPair;
}
@Override
public ExtendedJSONObject toJSONObject() {
ExtendedJSONObject o = super.toJSONObject();
// Fields are non-null by constructor.
o.put("sessionToken", Utils.byte2Hex(sessionToken));
o.put("kA", Utils.byte2Hex(kA));
o.put("kB", Utils.byte2Hex(kB));
o.put("keyPair", keyPair.toJSONObject());
return o;
}
@Override
public Action getNeededAction() {
return Action.None;
}
}

View File

@ -5,21 +5,29 @@
package org.mozilla.gecko.fxa.sync;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
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.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.SkewHandler;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
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;
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginDelegate;
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException;
import org.mozilla.gecko.fxa.authenticator.FxAccountLoginPolicy;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
@ -60,16 +68,21 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
protected static class SessionCallback implements BaseGlobalSessionCallback {
protected final CountDownLatch latch;
protected final SyncResult syncResult;
protected final AndroidFxAccount fxAccount;
public SessionCallback(CountDownLatch latch, SyncResult syncResult) {
public SessionCallback(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount) {
if (latch == null) {
throw new IllegalArgumentException("latch must not be null");
}
if (syncResult == null) {
throw new IllegalArgumentException("syncResult must not be null");
}
if (fxAccount == null) {
throw new IllegalArgumentException("fxAccount must not be null");
}
this.latch = latch;
this.syncResult = syncResult;
this.fxAccount = fxAccount;
}
@Override
@ -126,6 +139,16 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void handleError(GlobalSession globalSession, Exception e) {
// This is awful, but we need to propagate bad assertions back up the
// chain somehow, and this will do for now.
if (e instanceof TokenServerException) {
// We should only get here *after* we're locked into the married state.
State state = fxAccount.getState();
if (state.getStateLabel() == StateLabel.Married) {
Married married = (Married) state;
fxAccount.setState(married.makeCohabitingState());
}
}
setSyncResultSoftError();
Logger.warn(LOG_TAG, "Sync failed.", e);
latch.countDown();
@ -139,6 +162,48 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
}
};
protected void syncWithAssertion(final String audience, final String assertion, URI tokenServerEndpointURI, final String prefsPath, final SharedPreferences sharedPrefs, final KeyBundle syncKeyBundle, final BaseGlobalSessionCallback callback) {
TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new TokenServerClientDelegate() {
@Override
public void handleSuccess(final TokenServerToken token) {
FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
FxAccountGlobalSession globalSession = null;
try {
ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
// We compute skew over time using SkewHandler. This yields an unchanging
// skew adjustment that the HawkAuthHeaderProvider uses to adjust its
// timestamps. Eventually we might want this to adapt within the scope of a
// global session.
final SkewHandler tokenServerSkewHandler = SkewHandler.getSkewHandlerFromEndpointString(token.endpoint);
final long tokenServerSkew = tokenServerSkewHandler.getSkewInSeconds();
AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false, tokenServerSkew);
// EXTRAS
globalSession = new FxAccountGlobalSession(token.endpoint, token.uid, authHeaderProvider, prefsPath, syncKeyBundle, callback, getContext(), Bundle.EMPTY, clientsDataDelegate);
globalSession.start();
} catch (Exception e) {
callback.handleError(globalSession, e);
return;
}
}
@Override
public void handleFailure(TokenServerException e) {
debugAssertion(audience, assertion);
handleError(e);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Failed to get token.", e);
callback.handleError(null, e);
}
});
}
/**
* A trivial Sync implementation that does not cache client keys,
* certificates, or tokens.
@ -149,21 +214,28 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, SyncResult syncResult) {
Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
Logger.resetLogging();
Logger.info(LOG_TAG, "Syncing FxAccount" +
" account named " + account.name +
" for authority " + authority +
" with instance " + this + ".");
final Context context = getContext();
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
final CountDownLatch latch = new CountDownLatch(1);
final BaseGlobalSessionCallback callback = new SessionCallback(latch, syncResult);
final BaseGlobalSessionCallback callback = new SessionCallback(latch, syncResult, fxAccount);
try {
final Context context = getContext();
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
State state;
try {
state = fxAccount.getState();
} catch (Exception e) {
callback.handleError(null, e);
return;
}
final String prefsPath = fxAccount.getSyncPrefsPath();
@ -172,80 +244,61 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
final SharedPreferences sharedPrefs = context.getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE);
final String audience = fxAccount.getAudience();
final String authServerEndpoint = fxAccount.getAccountServerURI();
final String tokenServerEndpoint = fxAccount.getTokenServerURI();
final URI tokenServerEndpointURI = new URI(tokenServerEndpoint);
// TODO: why doesn't the loginPolicy extract the audience from the account?
final FxAccountLoginPolicy loginPolicy = new FxAccountLoginPolicy(context, 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(audience, new FxAccountLoginDelegate() {
final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor);
final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() {
@Override
public void handleSuccess(final String assertion) {
TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new TokenServerClientDelegate() {
@Override
public void handleSuccess(final TokenServerToken token) {
FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
sharedPrefs.edit().putLong("tokenFailures", 0).commit();
FxAccountGlobalSession globalSession = null;
try {
ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
// TODO Document this choice for deriving from kB.
final KeyBundle syncKeyBundle = FxAccountUtils.generateSyncKeyBundle(fxAccount.getKb());
// We compute skew over time using SkewHandler. This yields an unchanging
// skew adjustment that the HawkAuthHeaderProvider uses to adjust its
// timestamps. Eventually we might want this to adapt within the scope of a
// global session.
final SkewHandler tokenServerSkewHandler = SkewHandler.getSkewHandlerFromEndpointString(token.endpoint);
final long tokenServerSkew = tokenServerSkewHandler.getSkewInSeconds();
AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false, tokenServerSkew);
globalSession = new FxAccountGlobalSession(token.endpoint, token.uid, authHeaderProvider, prefsPath, syncKeyBundle, callback, context, extras, clientsDataDelegate);
globalSession.start();
} catch (Exception e) {
callback.handleError(globalSession, e);
return;
}
}
@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);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Failed to get token.", e);
callback.handleError(null, e);
}
});
public FxAccountClient getClient() {
return client;
}
@Override
public void handleError(FxAccountLoginException e) {
Logger.error(LOG_TAG, "Got error logging in.", e);
callback.handleError(null, e);
public long getCertificateDurationInMilliseconds() {
return 60 * 60 * 1000;
}
@Override
public long getAssertionDurationInMilliseconds() {
return 15 * 60 * 1000;
}
@Override
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return RSACryptoImplementation.generateKeyPair(1024);
}
@Override
public void handleTransition(Transition transition, State state) {
Logger.warn(LOG_TAG, "handleTransition: " + transition + " to " + state);
}
@Override
public void handleFinal(State state) {
Logger.warn(LOG_TAG, "handleFinal: in " + state);
fxAccount.setState(state);
try {
if (state.getStateLabel() != StateLabel.Married) {
callback.handleError(null, new RuntimeException("Cannot sync from state: " + state));
return;
}
Married married = (Married) state;
final long now = System.currentTimeMillis();
SkewHandler skewHandler = SkewHandler.getSkewHandlerFromEndpointString(tokenServerEndpoint);
String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER,
now + skewHandler.getSkewInMillis(),
this.getAssertionDurationInMilliseconds());
syncWithAssertion(audience, assertion, tokenServerEndpointURI, prefsPath, sharedPrefs, married.getSyncKeyBundle(), callback);
} catch (Exception e) {
callback.handleError(null, e);
return;
}
}
});

View File

@ -128,31 +128,43 @@
<LinearLayout
android:id="@+id/debug_buttons"
style="@style/FxAccountMiddle"
android:background="#7f7f7f" >
android:visibility="gone" >
<Button
android:id="@+id/debug_refresh_button"
style="@style/FxAccountButton"
android:layout_marginBottom="10dp"
android:onClick="onClickRefresh"
android:text="Refresh" />
android:text="@string/fxaccount_status_debug_refresh_button_label" />
<Button
android:id="@+id/debug_dump_button"
style="@style/FxAccountButton"
android:layout_marginBottom="10dp"
android:onClick="onClickDumpAccountDetails"
android:text="Dump Account Details" />
android:text="@string/fxaccount_status_debug_dump_button_label" />
<Button
android:id="@+id/debug_sync_button"
style="@style/FxAccountButton"
android:layout_marginBottom="10dp"
android:onClick="onClickForgetAccountTokens"
android:text="Forget sessionToken and keyFetchToken" />
android:text="@string/fxaccount_status_debug_sync_button_label" />
<Button
android:id="@+id/debug_forget_certificate_button"
style="@style/FxAccountButton"
android:layout_marginBottom="10dp"
android:onClick="onClickForgetPassword"
android:text="Forget password" />
android:text="@string/fxaccount_status_debug_forget_certificate_button_label" />
<Button
android:id="@+id/debug_require_password_button"
style="@style/FxAccountButton"
android:layout_marginBottom="10dp"
android:text="@string/fxaccount_status_debug_require_password_button_label" />
<Button
android:id="@+id/debug_require_upgrade_button"
style="@style/FxAccountButton"
android:layout_marginBottom="10dp"
android:text="@string/fxaccount_status_debug_require_upgrade_button_label" />
</LinearLayout>
</LinearLayout>

View File

@ -563,4 +563,12 @@ public class Utils {
}
return serverURL + "user/1.0/" + userPart;
}
public static void throwIfNull(Object... objects) {
for (Object object : objects) {
if (object == null) {
throw new IllegalArgumentException("object must not be null");
}
}
}
}

View File

@ -181,3 +181,9 @@
<!-- TODO: add email address to toast? -->
<string name="fxaccount_confirm_verification_link_sent">Sent fresh verification link</string>
<string name="fxaccount_confirm_verification_link_not_sent">Couldn\&apos;t send a fresh verification link</string>
<string name="fxaccount_status_debug_refresh_button_label">Refresh status view</string>
<string name="fxaccount_status_debug_dump_button_label">Dump account details</string>
<string name="fxaccount_status_debug_sync_button_label">Force sync</string>
<string name="fxaccount_status_debug_forget_certificate_button_label">Forget certificate (if applicable)</string>
<string name="fxaccount_status_debug_require_password_button_label">Require password re-entry</string>
<string name="fxaccount_status_debug_require_upgrade_button_label">Require upgrade</string>