mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 956839 - Implement Android native Firefox Account status UI. r=rnewman
This commit is contained in:
parent
af02f29c86
commit
2739a12677
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
mobile/android/base/background/fxa/FxAccountRemoteError.java
Normal file
30
mobile/android/base/background/fxa/FxAccountRemoteError.java
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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."));
|
||||
}
|
||||
}
|
||||
}
|
49
mobile/android/base/fxa/login/BaseRequestDelegate.java
Normal file
49
mobile/android/base/fxa/login/BaseRequestDelegate.java
Normal 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);
|
||||
}
|
||||
}
|
46
mobile/android/base/fxa/login/Cohabiting.java
Normal file
46
mobile/android/base/fxa/login/Cohabiting.java
Normal 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
25
mobile/android/base/fxa/login/Doghouse.java
Normal file
25
mobile/android/base/fxa/login/Doghouse.java
Normal 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;
|
||||
}
|
||||
}
|
83
mobile/android/base/fxa/login/Engaged.java
Normal file
83
mobile/android/base/fxa/login/Engaged.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
62
mobile/android/base/fxa/login/FxAccountLoginTransition.java
Normal file
62
mobile/android/base/fxa/login/FxAccountLoginTransition.java
Normal 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) + ")";
|
||||
}
|
||||
}
|
||||
}
|
102
mobile/android/base/fxa/login/Married.java
Normal file
102
mobile/android/base/fxa/login/Married.java
Normal 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);
|
||||
}
|
||||
}
|
59
mobile/android/base/fxa/login/Promised.java
Normal file
59
mobile/android/base/fxa/login/Promised.java
Normal 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;
|
||||
}
|
||||
}
|
25
mobile/android/base/fxa/login/Separated.java
Normal file
25
mobile/android/base/fxa/login/Separated.java
Normal 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;
|
||||
}
|
||||
}
|
71
mobile/android/base/fxa/login/State.java
Normal file
71
mobile/android/base/fxa/login/State.java
Normal 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();
|
||||
}
|
69
mobile/android/base/fxa/login/StateFactory.java
Normal file
69
mobile/android/base/fxa/login/StateFactory.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
41
mobile/android/base/fxa/login/TokensAndKeysState.java
Normal file
41
mobile/android/base/fxa/login/TokensAndKeysState.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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\'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>
|
||||
|
Loading…
Reference in New Issue
Block a user