/* 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.goanna.background.fxa; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import org.json.simple.JSONObject; import org.mozilla.goanna.background.common.log.Logger; import org.mozilla.goanna.background.fxa.FxAccountClientException.FxAccountClientRemoteException; import org.mozilla.goanna.sync.ExtendedJSONObject; import org.mozilla.goanna.sync.Utils; import org.mozilla.goanna.sync.net.BaseResource; import ch.boye.httpclientandroidlib.HttpResponse; public class FxAccountClient20 extends FxAccountClient10 implements FxAccountClient { protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN }; protected static final String[] LOGIN_RESPONSE_REQUIRED_STRING_FIELDS_KEYS = new String[] { JSON_KEY_UID, JSON_KEY_SESSIONTOKEN, JSON_KEY_KEYFETCHTOKEN, }; protected static final String[] LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS = new String[] { JSON_KEY_VERIFIED }; public FxAccountClient20(String serverURI, Executor executor) { super(serverURI, executor); } /** * Thin container for login response. *

* The remoteEmail field is the email address as normalized by the * server, and is not necessarily the email address delivered to the * login or create call. */ public static class LoginResponse { public final String remoteEmail; public final String uid; public final byte[] sessionToken; public final boolean verified; public final byte[] keyFetchToken; public LoginResponse(String remoteEmail, String uid, boolean verified, byte[] sessionToken, byte[] keyFetchToken) { this.remoteEmail = remoteEmail; this.uid = uid; this.verified = verified; this.sessionToken = sessionToken; this.keyFetchToken = keyFetchToken; } } // Public for testing only; prefer login and loginAndGetKeys (without boolean parameter). public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys, final Map queryParameters, final RequestDelegate delegate) { final BaseResource resource; final JSONObject body; try { final String path = "account/login"; final Map modifiedParameters = new HashMap<>(); if (queryParameters != null) { modifiedParameters.putAll(queryParameters); } if (getKeys) { modifiedParameters.put("keys", "true"); } resource = getBaseResource(path, modifiedParameters); body = new FxAccount20LoginDelegate(emailUTF8, quickStretchedPW).getCreateBody(); } catch (Exception e) { invokeHandleError(delegate, e); return; } resource.delegate = new ResourceDelegate(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); final String[] requiredBooleanFields = LOGIN_RESPONSE_REQUIRED_BOOLEAN_FIELDS; body.throwIfFieldsMissingOrMisTyped(requiredBooleanFields, Boolean.class); String uid = body.getString(JSON_KEY_UID); boolean verified = body.getBoolean(JSON_KEY_VERIFIED); 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); } public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys, final boolean preVerified, final Map queryParameters, final RequestDelegate delegate) { final BaseResource resource; final JSONObject body; try { final String path = "account/create"; final Map modifiedParameters = new HashMap<>(); if (queryParameters != null) { modifiedParameters.putAll(queryParameters); } if (getKeys) { modifiedParameters.put("keys", "true"); } resource = getBaseResource(path, modifiedParameters); 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(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; } 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); } catch (Exception e) { delegate.handleError(e); } } }; post(resource, body, delegate); } @Override public void createAccountAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, final Map queryParameters, RequestDelegate delegate) { try { byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(emailUTF8); createAccount(emailUTF8, quickStretchedPW, true, false, queryParameters, delegate); } catch (Exception e) { invokeHandleError(delegate, e); } } @Override public void loginAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, final Map queryParameters, RequestDelegate delegate) { login(emailUTF8, passwordStretcher, true, queryParameters, 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 login retries at most one time with a server provided email * address. *

* 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 keyFetchToken should be returned (in * addition to the standard sessionToken). * @param queryParameters * @param delegate * to invoke callbacks. */ public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys, final Map queryParameters, final RequestDelegate delegate) { byte[] quickStretchedPW; try { FxAccountUtils.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, queryParameters, new RequestDelegate() { @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."); FxAccountUtils.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, queryParameters, delegate); } catch (Exception innerException) { delegate.handleError(innerException); return; } } }); } }