michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.fxa.login; michael@0: michael@0: import java.security.NoSuchAlgorithmException; michael@0: import java.security.spec.InvalidKeySpecException; michael@0: michael@0: import org.mozilla.gecko.background.common.log.Logger; michael@0: import org.mozilla.gecko.browserid.BrowserIDKeyPair; michael@0: import org.mozilla.gecko.browserid.DSACryptoImplementation; michael@0: import org.mozilla.gecko.browserid.RSACryptoImplementation; michael@0: import org.mozilla.gecko.fxa.FxAccountConstants; michael@0: import org.mozilla.gecko.fxa.login.State.StateLabel; michael@0: import org.mozilla.gecko.sync.ExtendedJSONObject; michael@0: import org.mozilla.gecko.sync.NonObjectJSONException; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: michael@0: /** michael@0: * Create {@link State} instances from serialized representations. michael@0: *

michael@0: * Version 1 recognizes 5 state labels (Engaged, Cohabiting, Married, Separated, michael@0: * Doghouse). In the Cohabiting and Married states, the associated key pairs are michael@0: * always RSA key pairs. michael@0: *

michael@0: * Version 2 is identical to version 1, except that in the Cohabiting and michael@0: * Married states, the associated keypairs are always DSA key pairs. michael@0: */ michael@0: public class StateFactory { michael@0: private static final String LOG_TAG = StateFactory.class.getSimpleName(); michael@0: michael@0: private static final int KEY_PAIR_SIZE_IN_BITS_V1 = 1024; michael@0: michael@0: public static BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { michael@0: // New key pairs are always DSA. michael@0: return DSACryptoImplementation.generateKeyPair(KEY_PAIR_SIZE_IN_BITS_V1); michael@0: } michael@0: michael@0: protected static BrowserIDKeyPair keyPairFromJSONObjectV1(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { michael@0: // V1 key pairs are RSA. michael@0: return RSACryptoImplementation.fromJSONObject(o); michael@0: } michael@0: michael@0: protected static BrowserIDKeyPair keyPairFromJSONObjectV2(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { michael@0: // V2 key pairs are DSA. michael@0: return DSACryptoImplementation.fromJSONObject(o); michael@0: } michael@0: michael@0: public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { michael@0: Long version = o.getLong("version"); michael@0: if (version == null) { michael@0: throw new IllegalStateException("version must not be null"); michael@0: } michael@0: michael@0: final int v = version.intValue(); michael@0: if (v == 2) { michael@0: // The most common case is the most recent version. michael@0: return fromJSONObjectV2(stateLabel, o); michael@0: } michael@0: if (v == 1) { michael@0: final State state = fromJSONObjectV1(stateLabel, o); michael@0: return migrateV1toV2(stateLabel, state); michael@0: } michael@0: throw new IllegalStateException("version must be in {1, 2}"); michael@0: } michael@0: michael@0: protected static State fromJSONObjectV1(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { michael@0: switch (stateLabel) { michael@0: case Engaged: michael@0: return new Engaged( michael@0: o.getString("email"), michael@0: o.getString("uid"), michael@0: o.getBoolean("verified"), michael@0: Utils.hex2Byte(o.getString("unwrapkB")), michael@0: Utils.hex2Byte(o.getString("sessionToken")), michael@0: Utils.hex2Byte(o.getString("keyFetchToken"))); michael@0: case Cohabiting: michael@0: return new Cohabiting( michael@0: o.getString("email"), michael@0: o.getString("uid"), michael@0: Utils.hex2Byte(o.getString("sessionToken")), michael@0: Utils.hex2Byte(o.getString("kA")), michael@0: Utils.hex2Byte(o.getString("kB")), michael@0: keyPairFromJSONObjectV1(o.getObject("keyPair"))); michael@0: case Married: michael@0: return new Married( michael@0: o.getString("email"), michael@0: o.getString("uid"), michael@0: Utils.hex2Byte(o.getString("sessionToken")), michael@0: Utils.hex2Byte(o.getString("kA")), michael@0: Utils.hex2Byte(o.getString("kB")), michael@0: keyPairFromJSONObjectV1(o.getObject("keyPair")), michael@0: o.getString("certificate")); michael@0: case Separated: michael@0: return new Separated( michael@0: o.getString("email"), michael@0: o.getString("uid"), michael@0: o.getBoolean("verified")); michael@0: case Doghouse: michael@0: return new Doghouse( michael@0: o.getString("email"), michael@0: o.getString("uid"), michael@0: o.getBoolean("verified")); michael@0: default: michael@0: throw new IllegalStateException("unrecognized state label: " + stateLabel); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Exactly the same as {@link fromJSONObjectV1}, except that all key pairs are DSA key pairs. michael@0: */ michael@0: protected static State fromJSONObjectV2(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { michael@0: switch (stateLabel) { michael@0: case Cohabiting: michael@0: return new Cohabiting( michael@0: o.getString("email"), michael@0: o.getString("uid"), michael@0: Utils.hex2Byte(o.getString("sessionToken")), michael@0: Utils.hex2Byte(o.getString("kA")), michael@0: Utils.hex2Byte(o.getString("kB")), michael@0: keyPairFromJSONObjectV2(o.getObject("keyPair"))); michael@0: case Married: michael@0: return new Married( michael@0: o.getString("email"), michael@0: o.getString("uid"), michael@0: Utils.hex2Byte(o.getString("sessionToken")), michael@0: Utils.hex2Byte(o.getString("kA")), michael@0: Utils.hex2Byte(o.getString("kB")), michael@0: keyPairFromJSONObjectV2(o.getObject("keyPair")), michael@0: o.getString("certificate")); michael@0: default: michael@0: return fromJSONObjectV1(stateLabel, o); michael@0: } michael@0: } michael@0: michael@0: protected static void logMigration(State from, State to) { michael@0: if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) { michael@0: return; michael@0: } michael@0: try { michael@0: FxAccountConstants.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString()); michael@0: } catch (Exception e) { michael@0: Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e); michael@0: } michael@0: FxAccountConstants.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString()); michael@0: } michael@0: michael@0: protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException { michael@0: if (state == null) { michael@0: // This should never happen, but let's be careful. michael@0: Logger.error(LOG_TAG, "Got null state in migrateV1toV2; returning null."); michael@0: return state; michael@0: } michael@0: michael@0: Logger.info(LOG_TAG, "Migrating V1 persisted State to V2; stateLabel: " + stateLabel); michael@0: michael@0: // In V1, we use an RSA keyPair. In V2, we use a DSA keyPair. Only michael@0: // Cohabiting and Married states have a persisted keyPair at all; all michael@0: // other states need no conversion at all. michael@0: switch (stateLabel) { michael@0: case Cohabiting: { michael@0: // In the Cohabiting state, we can just generate a new key pair and move on. michael@0: final Cohabiting cohabiting = (Cohabiting) state; michael@0: final BrowserIDKeyPair keyPair = generateKeyPair(); michael@0: final State migrated = new Cohabiting(cohabiting.email, cohabiting.uid, cohabiting.sessionToken, cohabiting.kA, cohabiting.kB, keyPair); michael@0: logMigration(cohabiting, migrated); michael@0: return migrated; michael@0: } michael@0: case Married: { michael@0: // In the Married state, we cannot only change the key pair: the stored michael@0: // certificate signs the public key of the now obsolete key pair. We michael@0: // regress to the Cohabiting state; the next time we sync, we should michael@0: // advance back to Married. michael@0: final Married married = (Married) state; michael@0: final BrowserIDKeyPair keyPair = generateKeyPair(); michael@0: final State migrated = new Cohabiting(married.email, married.uid, married.sessionToken, married.kA, married.kB, keyPair); michael@0: logMigration(married, migrated); michael@0: return migrated; michael@0: } michael@0: default: michael@0: // Otherwise, V1 and V2 states are identical. michael@0: return state; michael@0: } michael@0: } michael@0: }