Bug 981827 - Make Android and Desktop FxAccounts client use same key parameters. r=rnewman, a=sledru
authorNick Alexander <nalexander@mozilla.com>
Wed, 09 Apr 2014 16:57:16 -0700
changeset 183744 13a97e892449319962dda00edfbf528e2ef01e5a
parent 183743 7872e02410a7012d625de794a319591540a1a38e
child 183745 4dd58172981ccebd2e663ec0862d6ffbc60adb59
push id3467
push userryanvm@gmail.com
push dateMon, 14 Apr 2014 14:29:30 +0000
treeherdermozilla-beta@756b592c869f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman, sledru
bugs981827
milestone29.0
Bug 981827 - Make Android and Desktop FxAccounts client use same key parameters. r=rnewman, a=sledru
mobile/android/base/fxa/login/State.java
mobile/android/base/fxa/login/StateFactory.java
mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
--- a/mobile/android/base/fxa/login/State.java
+++ b/mobile/android/base/fxa/login/State.java
@@ -4,17 +4,17 @@
 
 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 static final long CURRENT_VERSION = 2L;
 
   public enum StateLabel {
     Engaged,
     Cohabiting,
     Married,
     Separated,
     Doghouse,
   }
--- a/mobile/android/base/fxa/login/StateFactory.java
+++ b/mobile/android/base/fxa/login/StateFactory.java
@@ -2,61 +2,185 @@
  * 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.background.common.log.Logger;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.DSACryptoImplementation;
 import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.fxa.FxAccountConstants;
 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;
 
+/**
+ * Create {@link State} instances from serialized representations.
+ * <p>
+ * Version 1 recognizes 5 state labels (Engaged, Cohabiting, Married, Separated,
+ * Doghouse). In the Cohabiting and Married states, the associated key pairs are
+ * always RSA key pairs.
+ * <p>
+ * Version 2 is identical to version 1, except that in the Cohabiting and
+ * Married states, the associated keypairs are always DSA key pairs.
+ */
 public class StateFactory {
+  private static final String LOG_TAG = StateFactory.class.getSimpleName();
+
+  private static final int KEY_PAIR_SIZE_IN_BITS_V1 = 1024;
+
+  public static BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+    // New key pairs are always DSA.
+    return DSACryptoImplementation.generateKeyPair(KEY_PAIR_SIZE_IN_BITS_V1);
+  }
+
+  protected static BrowserIDKeyPair keyPairFromJSONObjectV1(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+    // V1 key pairs are RSA.
+    return RSACryptoImplementation.fromJSONObject(o);
+  }
+
+  protected static BrowserIDKeyPair keyPairFromJSONObjectV2(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
+    // V2 key pairs are DSA.
+    return DSACryptoImplementation.fromJSONObject(o);
+  }
+
   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");
+    if (version == null) {
+      throw new IllegalStateException("version must not be null");
+    }
+
+    final int v = version.intValue();
+    if (v == 2) {
+      // The most common case is the most recent version.
+      return fromJSONObjectV2(stateLabel, o);
     }
+    if (v == 1) {
+      final State state = fromJSONObjectV1(stateLabel, o);
+      return migrateV1toV2(stateLabel, state);
+    }
+    throw new IllegalStateException("version must be in {1, 2}");
+  }
+
+  protected static State fromJSONObjectV1(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
     switch (stateLabel) {
     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")));
+          keyPairFromJSONObjectV1(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")),
+          keyPairFromJSONObjectV1(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);
     }
   }
+
+  /**
+   * Exactly the same as {@link fromJSONObjectV1}, except that all key pairs are DSA key pairs.
+   */
+  protected static State fromJSONObjectV2(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+    switch (stateLabel) {
+    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")),
+          keyPairFromJSONObjectV2(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")),
+          keyPairFromJSONObjectV2(o.getObject("keyPair")),
+          o.getString("certificate"));
+    default:
+      return fromJSONObjectV1(stateLabel, o);
+    }
+  }
+
+  protected static void logMigration(State from, State to) {
+    if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+      return;
+    }
+    try {
+      FxAccountConstants.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString());
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e);
+    }
+    FxAccountConstants.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString());
+  }
+
+  protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException {
+    if (state == null) {
+      // This should never happen, but let's be careful.
+      Logger.error(LOG_TAG, "Got null state in migrateV1toV2; returning null.");
+      return state;
+    }
+
+    Logger.info(LOG_TAG, "Migrating V1 persisted State to V2; stateLabel: " + stateLabel);
+
+    // In V1, we use an RSA keyPair. In V2, we use a DSA keyPair. Only
+    // Cohabiting and Married states have a persisted keyPair at all; all
+    // other states need no conversion at all.
+    switch (stateLabel) {
+    case Cohabiting: {
+      // In the Cohabiting state, we can just generate a new key pair and move on.
+      final Cohabiting cohabiting = (Cohabiting) state;
+      final BrowserIDKeyPair keyPair = generateKeyPair();
+      final State migrated = new Cohabiting(cohabiting.email, cohabiting.uid, cohabiting.sessionToken, cohabiting.kA, cohabiting.kB, keyPair);
+      logMigration(cohabiting, migrated);
+      return migrated;
+    }
+    case Married: {
+      // In the Married state, we cannot only change the key pair: the stored
+      // certificate signs the public key of the now obsolete key pair. We
+      // regress to the Cohabiting state; the next time we sync, we should
+      // advance back to Married.
+      final Married married = (Married) state;
+      final BrowserIDKeyPair keyPair = generateKeyPair();
+      final State migrated = new Cohabiting(married.email, married.uid, married.sessionToken, married.kA, married.kB, keyPair);
+      logMigration(married, migrated);
+      return migrated;
+    }
+    default:
+      // Otherwise, V1 and V2 states are identical.
+      return state;
+    }
+  }
 }
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -12,29 +12,29 @@ import java.util.concurrent.ExecutorServ
 import java.util.concurrent.Executors;
 
 import org.mozilla.gecko.background.common.log.Logger;
 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.AccountPickler;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
 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.fxa.login.StateFactory;
 import org.mozilla.gecko.sync.BackoffHandler;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.PrefsBackoffHandler;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.ThreadPool;
 import org.mozilla.gecko.sync.Utils;
@@ -520,17 +520,17 @@ public class FxAccountSyncAdapter extend
 
         @Override
         public long getAssertionDurationInMilliseconds() {
           return 15 * 60 * 1000;
         }
 
         @Override
         public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
-          return RSACryptoImplementation.generateKeyPair(1024);
+          return StateFactory.generateKeyPair();
         }
 
         @Override
         public void handleTransition(Transition transition, State state) {
           Logger.info(LOG_TAG, "handleTransition: " + transition + " to " + state.getStateLabel());
         }
 
         private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) {