summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java')
-rw-r--r--mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java206
1 files changed, 206 insertions, 0 deletions
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java
new file mode 100644
index 000000000..a98f2fb27
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java
@@ -0,0 +1,206 @@
+/* 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.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.DSACryptoImplementation;
+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;
+
+/**
+ * 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) {
+ throw new IllegalStateException("version must not be null");
+ }
+
+ final int v = version.intValue();
+ if (v == 3) {
+ // The most common case is the most recent version.
+ return fromJSONObjectV3(stateLabel, o);
+ }
+ if (v == 2) {
+ 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")),
+ 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")),
+ 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);
+ }
+ }
+
+ /**
+ * Exactly the same as {@link fromJSONObjectV2}, except that there's a new
+ * MigratedFromSyncV11 state.
+ */
+ protected static State fromJSONObjectV3(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+ switch (stateLabel) {
+ case MigratedFromSync11:
+ return new MigratedFromSync11(
+ o.getString("email"),
+ o.getString("uid"),
+ o.getBoolean("verified"),
+ o.getString("password"));
+ default:
+ return fromJSONObjectV2(stateLabel, o);
+ }
+ }
+
+ protected static void logMigration(State from, State to) {
+ if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+ return;
+ }
+ try {
+ FxAccountUtils.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);
+ }
+ FxAccountUtils.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;
+ }
+ }
+}