Bug 963160 - Use server provided alternate email address. r=rnewman

This commit is contained in:
Nick Alexander 2014-01-25 18:06:55 -08:00
parent 119f895af4
commit 6625ad4d92
18 changed files with 412 additions and 290 deletions

View File

@ -510,6 +510,8 @@ sync_java_files = [
'background/fxa/FxAccountClientException.java',
'background/fxa/FxAccountRemoteError.java',
'background/fxa/FxAccountUtils.java',
'background/fxa/PasswordStretcher.java',
'background/fxa/QuickPasswordStretcher.java',
'background/fxa/SkewHandler.java',
'background/healthreport/Environment.java',
'background/healthreport/EnvironmentBuilder.java',
@ -568,7 +570,6 @@ sync_java_files = [
'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',

View File

@ -11,8 +11,10 @@ import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public interface FxAccountClient {
public void loginAndGetKeys(final byte[] emailUTF8, final byte[] quickStretchedPW, final RequestDelegate<LoginResponse> requestDelegate);
public void createAccountAndGetKeys(final byte[] emailUTF8, final PasswordStretcher passwordStretcher, final RequestDelegate<LoginResponse> delegate);
public void loginAndGetKeys(final byte[] emailUTF8, final PasswordStretcher passwordStretcher, final RequestDelegate<LoginResponse> requestDelegate);
public void status(byte[] sessionToken, RequestDelegate<StatusResponse> requestDelegate);
public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
public void resendCode(byte[] sessionToken, RequestDelegate<Void> delegate);
}

View File

@ -70,7 +70,14 @@ public class FxAccountClient10 {
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 };
/**
* The server's URI.
* <p>
* We assume throughout that this ends with a trailing slash (and guarantee as
* much in the constructor).
*/
protected final String serverURI;
protected final Executor executor;
public FxAccountClient10(String serverURI, Executor executor) {
@ -81,6 +88,9 @@ public class FxAccountClient10 {
throw new IllegalArgumentException("Must provide a non-null executor.");
}
this.serverURI = (serverURI.endsWith("/") ? serverURI : serverURI + "/") + VERSION_FRAGMENT;
if (!this.serverURI.endsWith("/")) {
throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
}
this.executor = executor;
}
@ -282,8 +292,9 @@ public class FxAccountClient10 {
String error;
String message;
String info;
ExtendedJSONObject body;
try {
ExtendedJSONObject body = new SyncStorageResponse(response).jsonObjectBody();
body = new SyncStorageResponse(response).jsonObjectBody();
body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
code = body.getLong(JSON_KEY_CODE).intValue();
@ -294,7 +305,7 @@ public class FxAccountClient10 {
} catch (Exception e) {
throw new FxAccountClientMalformedResponseException(response);
}
throw new FxAccountClientRemoteException(response, code, errno, error, message, info);
throw new FxAccountClientRemoteException(response, code, errno, error, message, info, body);
}
public void createAccount(final String email, final byte[] stretchedPWBytes,

View File

@ -8,6 +8,9 @@ import java.net.URI;
import java.util.concurrent.Executor;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.net.BaseResource;
@ -23,31 +26,22 @@ public class FxAccountClient20 extends FxAccountClient10 implements FxAccountCli
super(serverURI, executor);
}
public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean preVerified,
final RequestDelegate<String> delegate) {
try {
createAccount(new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified), delegate);
} catch (final Exception e) {
invokeHandleError(delegate, e);
return;
}
}
/**
* Thin container for login response.
* <p>
* The <code>remoteEmail</code> field is the email address as normalized by the
* server, and is <b>not necessarily</b> the email address delivered to the
* <code>login</code> or <code>create</code> call.
*/
public static class LoginResponse {
public final String serverURI;
public final String remoteEmail;
public final String uid;
public final byte[] sessionToken;
public final boolean verified;
public final byte[] keyFetchToken;
public LoginResponse(String serverURI, String uid, boolean verified, byte[] sessionToken, byte[] keyFetchToken) {
// This is pretty awful.
this.serverURI = serverURI.endsWith(VERSION_FRAGMENT) ?
serverURI.substring(0, serverURI.length() - VERSION_FRAGMENT.length()) :
serverURI;
public LoginResponse(String remoteEmail, String uid, boolean verified, byte[] sessionToken, byte[] keyFetchToken) {
this.remoteEmail = remoteEmail;
this.uid = uid;
this.verified = verified;
this.sessionToken = sessionToken;
@ -55,16 +49,6 @@ public class FxAccountClient20 extends FxAccountClient10 implements FxAccountCli
}
}
public void login(final byte[] emailUTF8, final byte[] quickStretchedPW,
final RequestDelegate<LoginResponse> delegate) {
login(emailUTF8, quickStretchedPW, false, delegate);
}
public void loginAndGetKeys(final byte[] emailUTF8, final byte[] quickStretchedPW,
final RequestDelegate<LoginResponse> delegate) {
login(emailUTF8, quickStretchedPW, true, delegate);
}
// Public for testing only; prefer login and loginAndGetKeys (without boolean parameter).
public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys,
final RequestDelegate<LoginResponse> delegate) {
@ -83,18 +67,12 @@ public class FxAccountClient20 extends FxAccountClient10 implements FxAccountCli
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
String[] requiredStringFields;
if (!getKeys) {
requiredStringFields = LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
} else {
requiredStringFields = LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS;
}
String[] requiredBooleanFields = LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS;
final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
final String[] requiredBooleanFields = LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredBooleanFields, Boolean.class);
LoginResponse loginResponse;
String uid = body.getString(JSON_KEY_UID);
boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN));
@ -102,7 +80,7 @@ public class FxAccountClient20 extends FxAccountClient10 implements FxAccountCli
if (getKeys) {
keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN));
}
loginResponse = new LoginResponse(serverURI, uid, verified, sessionToken, keyFetchToken);
LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken);
delegate.handleSuccess(loginResponse);
return;
@ -115,4 +93,137 @@ public class FxAccountClient20 extends FxAccountClient10 implements FxAccountCli
post(resource, body, delegate);
}
public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys, final boolean preVerified,
final RequestDelegate<LoginResponse> delegate) {
BaseResource resource;
JSONObject body;
final String path = getKeys ? "account/create?keys=true" : "account/create";
try {
resource = new BaseResource(new URI(serverURI + path));
body = new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified).getCreateBody();
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
// This is very similar to login, except verified is not required.
resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate) {
@Override
public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
try {
final String[] requiredStringFields = getKeys ? LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS : LOGIN_RESPONSE_REQUIRED_STRING_FIELDS;
body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
String uid = body.getString(JSON_KEY_UID);
boolean verified = false; // In production, we're definitely not verified immediately upon creation.
Boolean tempVerified = body.getBoolean(JSON_KEY_VERIFIED);
if (tempVerified != null) {
verified = tempVerified.booleanValue();
}
byte[] sessionToken = Utils.hex2Byte(body.getString(JSON_KEY_SESSIONTOKEN));
byte[] keyFetchToken = null;
if (getKeys) {
keyFetchToken = Utils.hex2Byte(body.getString(JSON_KEY_KEYFETCHTOKEN));
}
LoginResponse loginResponse = new LoginResponse(new String(emailUTF8, "UTF-8"), uid, verified, sessionToken, keyFetchToken);
delegate.handleSuccess(loginResponse);
return;
} catch (Exception e) {
delegate.handleError(e);
return;
}
}
};
post(resource, body, delegate);
}
@Override
public void createAccountAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, RequestDelegate<LoginResponse> delegate) {
try {
byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(emailUTF8);
createAccount(emailUTF8, quickStretchedPW, true, false, delegate);
} catch (Exception e) {
invokeHandleError(delegate, e);
return;
}
}
@Override
public void loginAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, RequestDelegate<LoginResponse> delegate) {
login(emailUTF8, passwordStretcher, true, delegate);
}
/**
* We want users to be able to enter their email address case-insensitively.
* We stretch the password locally using the email address as a salt, to make
* dictionary attacks more expensive. This means that a client with a
* case-differing email address is unable to produce the correct
* authorization, even though it knows the password. In this case, the server
* returns the email that the account was created with, so that the client can
* re-stretch the password locally with the correct email salt. This version
* of <code>login</code> retries at most one time with a server provided email
* address.
* <p>
* Be aware that consumers will not see the initial error response from the
* server providing an alternate email (if there is one).
*
* @param emailUTF8
* user entered email address.
* @param stretcher
* delegate to stretch and re-stretch password.
* @param getKeys
* true if a <code>keyFetchToken</code> should be returned (in
* addition to the standard <code>sessionToken</code>).
* @param delegate
* to invoke callbacks.
*/
public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys,
final RequestDelegate<LoginResponse> delegate) {
byte[] quickStretchedPW;
try {
FxAccountConstants.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" );
quickStretchedPW = stretcher.getQuickStretchedPW(emailUTF8);
} catch (Exception e) {
delegate.handleError(e);
return;
}
this.login(emailUTF8, quickStretchedPW, getKeys, new RequestDelegate<LoginResponse>() {
@Override
public void handleSuccess(LoginResponse result) {
delegate.handleSuccess(result);
}
@Override
public void handleError(Exception e) {
delegate.handleError(e);
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
String alternateEmail = e.body.getString(JSON_KEY_EMAIL);
if (!e.isBadEmailCase() || alternateEmail == null) {
delegate.handleFailure(e);
return;
};
Logger.info(LOG_TAG, "Server returned alternate email; retrying login with provided email.");
FxAccountConstants.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" );
try {
// Nota bene: this is not recursive, since we call the fixed password
// signature here, which invokes a non-retrying version.
byte[] alternateEmailUTF8 = alternateEmail.getBytes("UTF-8");
byte[] alternateQuickStretchedPW = stretcher.getQuickStretchedPW(alternateEmailUTF8);
login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, delegate);
} catch (Exception innerException) {
delegate.handleError(innerException);
return;
}
}
});
}
}

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.background.fxa;
import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
@ -34,15 +35,20 @@ public class FxAccountClientException extends Exception {
public final String error;
public final String message;
public final String info;
public final ExtendedJSONObject body;
public FxAccountClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, String info) {
public FxAccountClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, String info, ExtendedJSONObject body) {
super(new HTTPFailureException(new SyncStorageResponse(response)));
if (body == null) {
throw new IllegalArgumentException("body must not be null");
}
this.response = response;
this.httpStatusCode = httpStatusCode;
this.apiErrorNumber = apiErrorNumber;
this.error = error;
this.message = message;
this.info = info;
this.body = body;
}
@Override
@ -55,6 +61,10 @@ public class FxAccountClientException extends Exception {
}
public boolean isAccountAlreadyExists() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
}
public boolean isAccountDoesNotExist() {
return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
}
@ -74,35 +84,44 @@ public class FxAccountClientException extends Exception {
apiErrorNumber == FxAccountRemoteError.INCORRECT_API_VERSION_FOR_THIS_ACCOUNT;
}
public boolean isTooManyRequests() {
return apiErrorNumber == FxAccountRemoteError.CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
}
public boolean isServerUnavailable() {
return apiErrorNumber == FxAccountRemoteError.SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD;
}
public boolean isBadEmailCase() {
return apiErrorNumber == FxAccountRemoteError.INCORRECT_EMAIL_CASE;
}
public int getErrorMessageStringResource() {
if (isUpgradeRequired()) {
return R.string.fxaccount_remote_error_UPGRADE_REQUIRED;
}
switch ((int) apiErrorNumber) {
case FxAccountRemoteError.ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS:
} else if (isAccountAlreadyExists()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
case FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST:
} else if (isAccountDoesNotExist()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
case FxAccountRemoteError.INCORRECT_PASSWORD:
} else if (isBadPassword()) {
return R.string.fxaccount_remote_error_INCORRECT_PASSWORD;
case FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT:
} else if (isUnverified()) {
return R.string.fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
case FxAccountRemoteError.CLIENT_HAS_SENT_TOO_MANY_REQUESTS:
} else if (isTooManyRequests()) {
return R.string.fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
case FxAccountRemoteError.SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD:
} else if (isServerUnavailable()) {
return R.string.fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;
default:
} else {
return R.string.fxaccount_remote_error_UNKNOWN_ERROR;
}
}
}
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");
super(response, 0, FxAccountRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", "Response malformed", new ExtendedJSONObject());
}
}
}

View File

@ -25,6 +25,7 @@ public interface FxAccountRemoteError {
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 INCORRECT_EMAIL_CASE = 120;
public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201;
public static final int UNKNOWN_ERROR = 999;
}

View File

@ -0,0 +1,12 @@
/* 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;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
public interface PasswordStretcher {
public byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException;
}

View File

@ -0,0 +1,34 @@
/* 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;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.sync.Utils;
public class QuickPasswordStretcher implements PasswordStretcher {
protected final String password;
protected final Map<String, String> cache = new HashMap<String, String>();
public QuickPasswordStretcher(String password) {
this.password = password;
}
public synchronized byte[] getQuickStretchedPW(byte[] emailUTF8) throws UnsupportedEncodingException, GeneralSecurityException {
if (emailUTF8 == null) {
throw new IllegalArgumentException("emailUTF8 must not be null");
}
String key = Utils.byte2Hex(emailUTF8);
if (!cache.containsKey(key)) {
byte[] value = FxAccountUtils.generateQuickStretchedPW(emailUTF8, password.getBytes("UTF-8"));
cache.put(key, Utils.byte2Hex(value));
return value;
}
return Utils.hex2Byte(cache.get(key));
}
}

View File

@ -11,7 +11,7 @@ public class FxAccountConstants {
public static final String GLOBAL_LOG_TAG = "FxAccounts";
public static final String ACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@";
public static final String DEFAULT_IDP_ENDPOINT = "https://api-accounts.dev.lcip.org";
public static final String DEFAULT_IDP_ENDPOINT = "https://api-accounts-latest.dev.lcip.org";
public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.dev.lcip.org";
public static final String DEFAULT_TOKEN_SERVER_URI = DEFAULT_TOKEN_SERVER_ENDPOINT +
(DEFAULT_TOKEN_SERVER_ENDPOINT.endsWith("/") ? "" : "/") + "1.0/sync/1.5";

View File

@ -8,9 +8,20 @@ import java.io.IOException;
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.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.AccountManager;
import android.content.Intent;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
@ -164,4 +175,83 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
progressBar.setVisibility(View.INVISIBLE);
button.setVisibility(View.VISIBLE);
}
public Intent makeSuccessIntent(String email, LoginResponse result) {
Intent successIntent;
if (result.verified) {
successIntent = new Intent(this, FxAccountVerifiedAccountActivity.class);
} else {
successIntent = new Intent(this, FxAccountConfirmAccountActivity.class);
successIntent.putExtra("sessionToken", result.sessionToken);
}
successIntent.putExtra("email", email);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
return successIntent;
}
protected abstract class AddAccountDelegate implements RequestDelegate<LoginResponse> {
public final String email;
public final PasswordStretcher passwordStretcher;
public final String serverURI;
public AddAccountDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
this.email = email;
this.passwordStretcher = passwordStretcher;
this.serverURI = serverURI;
}
@Override
public void handleSuccess(LoginResponse result) {
Logger.info(LOG_TAG, "Got success response; adding Android account.");
// We're on the UI thread, but it's okay to create the account here.
AndroidFxAccount fxAccount;
try {
final String profile = Constants.DEFAULT_PROFILE;
final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_URI;
// It is crucial that we use the email address provided by the server
// (rather than whatever the user entered), because the user's keys are
// wrapped and salted with the initial email they provided to
// /create/account. Of course, we want to pass through what the user
// entered locally as much as possible, so we create the Android account
// with their entered email address, etc.
// The passwordStretcher should have seen this email address before, so
// we shouldn't be calculating the expensive stretch twice.
byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.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(getApplicationContext(),
email,
profile,
serverURI,
tokenServerURI,
state);
if (fxAccount == null) {
throw new RuntimeException("Could not add Android account.");
}
} catch (Exception e) {
handleError(e);
return;
}
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
// The GetStarted activity has called us and needs to return a result to the authenticator.
final Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
// intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType);
setResult(RESULT_OK, intent);
// Show success activity depending on verification status.
Intent successIntent = makeSuccessIntent(email, result);
startActivity(successIntent);
finish();
}
}
}

View File

@ -9,6 +9,7 @@ import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
@ -85,7 +86,7 @@ public class FxAccountConfirmAccountActivity extends Activity implements OnClick
protected final byte[] sessionToken;
public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient20 client, RequestDelegate<Void> delegate) {
public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient client, RequestDelegate<Void> delegate) {
super(context, null, client, delegate);
this.sessionToken = sessionToken;
}
@ -126,7 +127,7 @@ public class FxAccountConfirmAccountActivity extends Activity implements OnClick
String serverURI = FxAccountConstants.DEFAULT_IDP_ENDPOINT;
RequestDelegate<Void> delegate = new ResendCodeDelegate();
Executor executor = Executors.newSingleThreadExecutor();
FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
FxAccountClient client = new FxAccountClient20(serverURI, executor);
new FxAccountResendCodeTask(this, sessionToken, client, delegate).execute();
}

View File

@ -10,19 +10,16 @@ import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
import org.mozilla.gecko.background.fxa.FxAccountClient;
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.background.fxa.PasswordStretcher;
import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
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.fxa.login.Promised;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
@ -134,85 +131,28 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
});
}
protected class CreateAccountDelegate implements RequestDelegate<String> {
public final String email;
public final String password;
public final String serverURI;
public CreateAccountDelegate(String email, String password, String serverURI) {
this.email = email;
this.password = password;
this.serverURI = serverURI;
}
@Override
public void handleError(Exception e) {
showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
}
@Override
public void handleFailure(final FxAccountClientRemoteException e) {
showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
}
@Override
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.
AndroidFxAccount fxAccount;
try {
final String profile = Constants.DEFAULT_PROFILE;
final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_URI;
// 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,
state);
if (fxAccount == null) {
throw new RuntimeException("Could not add Android account.");
}
} catch (Exception e) {
handleError(e);
return;
}
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
// The GetStarted activity has called us and needs to return a result to the authenticator.
final Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
// intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType);
setResult(RESULT_OK, intent);
finish();
// Show success activity.
Intent successIntent = new Intent(FxAccountCreateAccountActivity.this, FxAccountConfirmAccountActivity.class);
successIntent.putExtra("email", email);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(successIntent);
}
}
public void createAccount(String email, String password) {
String serverURI = FxAccountConstants.DEFAULT_IDP_ENDPOINT;
RequestDelegate<String> delegate = new CreateAccountDelegate(email, password, serverURI);
PasswordStretcher passwordStretcher = new QuickPasswordStretcher(password);
// This delegate creates a new Android account on success, opens the
// appropriate "success!" activity, and finishes this activity.
RequestDelegate<LoginResponse> delegate = new AddAccountDelegate(email, passwordStretcher, serverURI) {
@Override
public void handleError(Exception e) {
showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
}
};
Executor executor = Executors.newSingleThreadExecutor();
FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
FxAccountClient client = new FxAccountClient20(serverURI, executor);
try {
hideRemoteError();
new FxAccountCreateAccountTask(this, this, email, password, client, delegate).execute();
new FxAccountCreateAccountTask(this, this, email, passwordStretcher, client, delegate).execute();
} catch (Exception e) {
showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
}

View File

@ -5,15 +5,14 @@
package org.mozilla.gecko.fxa.activities;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.concurrent.CountDownLatch;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
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.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.InnerRequestDelegate;
import android.content.Context;
@ -37,7 +36,7 @@ abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestD
}
protected final Context context;
protected final FxAccountClient20 client;
protected final FxAccountClient client;
protected final ProgressDisplay progressDisplay;
// Initialized lazily.
@ -49,7 +48,7 @@ abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestD
protected final RequestDelegate<T> delegate;
public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient20 client, RequestDelegate<T> delegate) {
public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient client, RequestDelegate<T> delegate) {
this.context = context;
this.client = client;
this.delegate = delegate;
@ -119,36 +118,22 @@ abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestD
}
}
public static class FxAccountCreateAccountTask extends FxAccountSetupTask<String> {
public static class FxAccountCreateAccountTask extends FxAccountSetupTask<LoginResponse> {
private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
protected final byte[] emailUTF8;
protected final byte[] passwordUTF8;
protected final PasswordStretcher passwordStretcher;
public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, String password, FxAccountClient20 client, RequestDelegate<String> delegate) throws UnsupportedEncodingException {
public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
super(context, progressDisplay, client, delegate);
this.emailUTF8 = email.getBytes("UTF-8");
this.passwordUTF8 = password.getBytes("UTF-8");
}
/**
* Stretching the password is expensive, so we compute the stretched value lazily.
*
* @return stretched password.
* @throws GeneralSecurityException
* @throws UnsupportedEncodingException
*/
public byte[] generateQuickStretchedPW() throws UnsupportedEncodingException, GeneralSecurityException {
if (this.quickStretchedPW == null) {
this.quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(emailUTF8, passwordUTF8);
}
return this.quickStretchedPW;
this.passwordStretcher = passwordStretcher;
}
@Override
protected InnerRequestDelegate<String> doInBackground(Void... arg0) {
protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
try {
client.createAccount(emailUTF8, generateQuickStretchedPW(), false, innerDelegate);
client.createAccountAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {
@ -163,32 +148,18 @@ abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestD
protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
protected final byte[] emailUTF8;
protected final byte[] passwordUTF8;
protected final PasswordStretcher passwordStretcher;
public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, String password, FxAccountClient20 client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
super(context, progressDisplay, client, delegate);
this.emailUTF8 = email.getBytes("UTF-8");
this.passwordUTF8 = password.getBytes("UTF-8");
}
/**
* Stretching the password is expensive, so we compute the stretched value lazily.
*
* @return stretched password.
* @throws GeneralSecurityException
* @throws UnsupportedEncodingException
*/
public byte[] generateQuickStretchedPW() throws UnsupportedEncodingException, GeneralSecurityException {
if (this.quickStretchedPW == null) {
this.quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(emailUTF8, passwordUTF8);
}
return this.quickStretchedPW;
this.passwordStretcher = passwordStretcher;
}
@Override
protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
try {
client.loginAndGetKeys(emailUTF8, generateQuickStretchedPW(), innerDelegate);
client.loginAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {

View File

@ -9,20 +9,16 @@ import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
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.background.fxa.PasswordStretcher;
import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
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.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.sync.setup.Constants;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
@ -103,91 +99,28 @@ public class FxAccountSignInActivity extends FxAccountAbstractSetupActivity {
this.finish();
}
protected class SignInDelegate implements RequestDelegate<LoginResponse> {
public final String email;
public final String password;
public final String serverURI;
public SignInDelegate(String email, String password, String serverURI) {
this.email = email;
this.password = password;
this.serverURI = serverURI;
}
@Override
public void handleError(Exception e) {
showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
}
@Override
public void handleSuccess(LoginResponse result) {
Activity activity = FxAccountSignInActivity.this;
Logger.info(LOG_TAG, "Got success signing in.");
// We're on the UI thread, but it's okay to create the account here.
AndroidFxAccount fxAccount;
try {
final String profile = Constants.DEFAULT_PROFILE;
final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_URI;
// 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,
state);
if (fxAccount == null) {
throw new RuntimeException("Could not add Android account.");
}
} catch (Exception e) {
handleError(e);
return;
}
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
// The GetStarted activity has called us and needs to return a result to the authenticator.
final Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
// intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType);
setResult(RESULT_OK, intent);
finish();
// Show success activity depending on verification status.
Intent successIntent;
if (result.verified) {
successIntent = new Intent(FxAccountSignInActivity.this, FxAccountVerifiedAccountActivity.class);
} else {
successIntent = new Intent(FxAccountSignInActivity.this, FxAccountConfirmAccountActivity.class);
successIntent.putExtra("sessionToken", result.sessionToken);
}
successIntent.putExtra("email", email);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with
// the soft keyboard not being shown for the started activity. Why, Android, why?
successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(successIntent);
}
}
public void signIn(String email, String password) {
String serverURI = FxAccountConstants.DEFAULT_IDP_ENDPOINT;
RequestDelegate<LoginResponse> delegate = new SignInDelegate(email, password, serverURI);
PasswordStretcher passwordStretcher = new QuickPasswordStretcher(password);
// This delegate creates a new Android account on success, opens the
// appropriate "success!" activity, and finishes this activity.
RequestDelegate<LoginResponse> delegate = new AddAccountDelegate(email, passwordStretcher, serverURI) {
@Override
public void handleError(Exception e) {
showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
}
};
Executor executor = Executors.newSingleThreadExecutor();
FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
FxAccountClient client = new FxAccountClient20(serverURI, executor);
try {
hideRemoteError();
new FxAccountSignInTask(this, this, email, password, client, delegate).execute();
new FxAccountSignInTask(this, this, email, passwordStretcher, client, delegate).execute();
} catch (Exception e) {
showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
}

View File

@ -4,18 +4,19 @@
package org.mozilla.gecko.fxa.activities;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
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.background.fxa.PasswordStretcher;
import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
@ -110,16 +111,13 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
protected class UpdateCredentialsDelegate implements RequestDelegate<LoginResponse> {
public final String email;
public final String password;
public final String serverURI;
public final byte[] quickStretchedPW;
public final PasswordStretcher passwordStretcher;
public UpdateCredentialsDelegate(String email, String password, String serverURI) throws UnsupportedEncodingException, GeneralSecurityException {
public UpdateCredentialsDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
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"));
this.passwordStretcher = passwordStretcher;
}
@Override
@ -144,6 +142,12 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
byte[] unwrapkB;
try {
// It is crucial that we use the email address provided by the server
// (rather than whatever the user entered), because the user's keys are
// wrapped and salted with the initial email they provided to
// /create/account. Of course, we want to pass through what the user
// entered locally as much as possible.
byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
} catch (Exception e) {
this.handleError(e);
@ -163,11 +167,12 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
public void updateCredentials(String email, String password) {
String serverURI = FxAccountConstants.DEFAULT_IDP_ENDPOINT;
Executor executor = Executors.newSingleThreadExecutor();
FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
FxAccountClient client = new FxAccountClient20(serverURI, executor);
PasswordStretcher passwordStretcher = new QuickPasswordStretcher(password);
try {
hideRemoteError();
RequestDelegate<LoginResponse> delegate = new UpdateCredentialsDelegate(email, password, serverURI);
new FxAccountSignInTask(this, this, email, password, client, delegate).execute();
RequestDelegate<LoginResponse> delegate = new UpdateCredentialsDelegate(email, passwordStretcher, serverURI);
new FxAccountSignInTask(this, this, email, passwordStretcher, client, delegate).execute();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception updating credentials for account.", e);
showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);

View File

@ -262,7 +262,6 @@ public class AndroidFxAccount {
public static AndroidFxAccount addAndroidAccount(
Context context,
String email,
String password,
String profile,
String idpServerURI,
String tokenServerURI,
@ -271,9 +270,6 @@ public class AndroidFxAccount {
if (email == null) {
throw new IllegalArgumentException("email must not be null");
}
if (password == null) {
throw new IllegalArgumentException("password must not be null");
}
if (idpServerURI == null) {
throw new IllegalArgumentException("idpServerURI must not be null");
}
@ -303,7 +299,10 @@ public class AndroidFxAccount {
Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
AccountManager accountManager = AccountManager.get(context);
boolean added = accountManager.addAccountExplicitly(account, null, userdata); // XXX what should the password be?
// We don't set an Android password, because we don't want to persist the
// password (or anything else as powerful as the password). Instead, we
// internally manage a sessionToken with a remotely owned lifecycle.
boolean added = accountManager.addAccountExplicitly(account, null, userdata);
if (!added) {
return null;
}

View File

@ -12,7 +12,6 @@ public abstract class State {
public static final long CURRENT_VERSION = 1L;
public enum StateLabel {
Promised,
Engaged,
Cohabiting,
Married,

View File

@ -20,13 +20,6 @@ public class StateFactory {
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"),