Bug 1240919 - Part 4a: Merge FxAccountClient20 into Client10, in preparation for swap. r=rnewman
authorNick Alexander <nalexander@mozilla.com>
Tue, 19 Jan 2016 14:16:41 -0800
changeset 323663 f6d45cf07a4a65e129c10eaeadf1867d026fc045
parent 323662 902120f314e9bd79dd1fa447688b8ea8adab9f6c
child 323664 a4056a35eaa108d44ab669cae0ce9e871c555fc3
push id9762
push usermozilla@noorenberghe.ca
push dateWed, 20 Jan 2016 21:31:00 +0000
reviewersrnewman
bugs1240919
milestone46.0a1
Bug 1240919 - Part 4a: Merge FxAccountClient20 into Client10, in preparation for swap. r=rnewman
mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient10.java
mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient10.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient10.java
@@ -1,15 +1,16 @@
 /* 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 org.json.simple.JSONObject;
+import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClientException
         .FxAccountClientMalformedResponseException;
 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.crypto.HKDF;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
@@ -24,16 +25,17 @@ import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLEncoder;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.concurrent.Executor;
 
 import javax.crypto.Mac;
 
 import ch.boye.httpclientandroidlib.HttpEntity;
@@ -537,9 +539,216 @@ public class FxAccountClient10 {
           delegate.handleError(new FxAccountClientException("cert must be a non-null string"));
           return;
         }
         delegate.handleSuccess(cert);
       }
     };
     post(resource, body, delegate);
   }
+
+  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 };
+
+  /**
+   * 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 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<String, String> queryParameters,
+                    final RequestDelegate<LoginResponse> delegate) {
+    final BaseResource resource;
+    final JSONObject body;
+    try {
+      final String path = "account/login";
+      final Map<String, String> 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<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);
+
+          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<String, String> queryParameters,
+                            final RequestDelegate<LoginResponse> delegate) {
+    final BaseResource resource;
+    final JSONObject body;
+    try {
+      final String path = "account/create";
+      final Map<String, String> 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<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;
+          }
+          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);
+  }
+
+  /**
+   * 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 queryParameters
+   * @param delegate
+   *          to invoke callbacks.
+   */
+  public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys,
+                    final Map<String, String> queryParameters,
+                    final RequestDelegate<LoginResponse> 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<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.");
+        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;
+        }
+      }
+    });
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/background/fxa/FxAccountClient20.java
@@ -13,219 +13,12 @@ import org.mozilla.gecko.background.comm
 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.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.
-   * <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 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<String, String> queryParameters,
-      final RequestDelegate<LoginResponse> delegate) {
-    final BaseResource resource;
-    final JSONObject body;
-    try {
-      final String path = "account/login";
-      final Map<String, String> 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<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);
-
-          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<String, String> queryParameters,
-      final RequestDelegate<LoginResponse> delegate) {
-    final BaseResource resource;
-    final JSONObject body;
-    try {
-      final String path = "account/create";
-      final Map<String, String> 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<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;
-          }
-          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);
-  }
-
-  /**
-   * 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 queryParameters
-   * @param delegate
-   *          to invoke callbacks.
-   */
-  public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys,
-      final Map<String, String> queryParameters,
-      final RequestDelegate<LoginResponse> 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<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.");
-        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;
-        }
-      }
-    });
-  }
 }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
@@ -1,36 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.fxa.login;
 
-import ch.boye.httpclientandroidlib.HttpStatus;
-import ch.boye.httpclientandroidlib.ProtocolVersion;
-import ch.boye.httpclientandroidlib.entity.StringEntity;
-import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
-import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClient10.LoginResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
-import org.mozilla.gecko.background.fxa.PasswordStretcher;
 import org.mozilla.gecko.browserid.MockMyIDTokenFactory;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
 import java.io.UnsupportedEncodingException;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.ProtocolVersion;
+import ch.boye.httpclientandroidlib.entity.StringEntity;
+import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
+
 public class MockFxAccountClient implements FxAccountClient {
   protected static MockMyIDTokenFactory mockMyIdTokenFactory = new MockMyIDTokenFactory();
 
   public final String serverURI = "http://testServer.com";
 
   public final Map<String, User> users = new HashMap<String, User>();
   public final Map<String, String> sessionTokens = new HashMap<String, String>();
   public final Map<String, String> keyFetchTokens = new HashMap<String, String>();