1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/fxa/login/StateFactory.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,186 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.fxa.login; 1.9 + 1.10 +import java.security.NoSuchAlgorithmException; 1.11 +import java.security.spec.InvalidKeySpecException; 1.12 + 1.13 +import org.mozilla.gecko.background.common.log.Logger; 1.14 +import org.mozilla.gecko.browserid.BrowserIDKeyPair; 1.15 +import org.mozilla.gecko.browserid.DSACryptoImplementation; 1.16 +import org.mozilla.gecko.browserid.RSACryptoImplementation; 1.17 +import org.mozilla.gecko.fxa.FxAccountConstants; 1.18 +import org.mozilla.gecko.fxa.login.State.StateLabel; 1.19 +import org.mozilla.gecko.sync.ExtendedJSONObject; 1.20 +import org.mozilla.gecko.sync.NonObjectJSONException; 1.21 +import org.mozilla.gecko.sync.Utils; 1.22 + 1.23 +/** 1.24 + * Create {@link State} instances from serialized representations. 1.25 + * <p> 1.26 + * Version 1 recognizes 5 state labels (Engaged, Cohabiting, Married, Separated, 1.27 + * Doghouse). In the Cohabiting and Married states, the associated key pairs are 1.28 + * always RSA key pairs. 1.29 + * <p> 1.30 + * Version 2 is identical to version 1, except that in the Cohabiting and 1.31 + * Married states, the associated keypairs are always DSA key pairs. 1.32 + */ 1.33 +public class StateFactory { 1.34 + private static final String LOG_TAG = StateFactory.class.getSimpleName(); 1.35 + 1.36 + private static final int KEY_PAIR_SIZE_IN_BITS_V1 = 1024; 1.37 + 1.38 + public static BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { 1.39 + // New key pairs are always DSA. 1.40 + return DSACryptoImplementation.generateKeyPair(KEY_PAIR_SIZE_IN_BITS_V1); 1.41 + } 1.42 + 1.43 + protected static BrowserIDKeyPair keyPairFromJSONObjectV1(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { 1.44 + // V1 key pairs are RSA. 1.45 + return RSACryptoImplementation.fromJSONObject(o); 1.46 + } 1.47 + 1.48 + protected static BrowserIDKeyPair keyPairFromJSONObjectV2(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { 1.49 + // V2 key pairs are DSA. 1.50 + return DSACryptoImplementation.fromJSONObject(o); 1.51 + } 1.52 + 1.53 + public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { 1.54 + Long version = o.getLong("version"); 1.55 + if (version == null) { 1.56 + throw new IllegalStateException("version must not be null"); 1.57 + } 1.58 + 1.59 + final int v = version.intValue(); 1.60 + if (v == 2) { 1.61 + // The most common case is the most recent version. 1.62 + return fromJSONObjectV2(stateLabel, o); 1.63 + } 1.64 + if (v == 1) { 1.65 + final State state = fromJSONObjectV1(stateLabel, o); 1.66 + return migrateV1toV2(stateLabel, state); 1.67 + } 1.68 + throw new IllegalStateException("version must be in {1, 2}"); 1.69 + } 1.70 + 1.71 + protected static State fromJSONObjectV1(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { 1.72 + switch (stateLabel) { 1.73 + case Engaged: 1.74 + return new Engaged( 1.75 + o.getString("email"), 1.76 + o.getString("uid"), 1.77 + o.getBoolean("verified"), 1.78 + Utils.hex2Byte(o.getString("unwrapkB")), 1.79 + Utils.hex2Byte(o.getString("sessionToken")), 1.80 + Utils.hex2Byte(o.getString("keyFetchToken"))); 1.81 + case Cohabiting: 1.82 + return new Cohabiting( 1.83 + o.getString("email"), 1.84 + o.getString("uid"), 1.85 + Utils.hex2Byte(o.getString("sessionToken")), 1.86 + Utils.hex2Byte(o.getString("kA")), 1.87 + Utils.hex2Byte(o.getString("kB")), 1.88 + keyPairFromJSONObjectV1(o.getObject("keyPair"))); 1.89 + case Married: 1.90 + return new Married( 1.91 + o.getString("email"), 1.92 + o.getString("uid"), 1.93 + Utils.hex2Byte(o.getString("sessionToken")), 1.94 + Utils.hex2Byte(o.getString("kA")), 1.95 + Utils.hex2Byte(o.getString("kB")), 1.96 + keyPairFromJSONObjectV1(o.getObject("keyPair")), 1.97 + o.getString("certificate")); 1.98 + case Separated: 1.99 + return new Separated( 1.100 + o.getString("email"), 1.101 + o.getString("uid"), 1.102 + o.getBoolean("verified")); 1.103 + case Doghouse: 1.104 + return new Doghouse( 1.105 + o.getString("email"), 1.106 + o.getString("uid"), 1.107 + o.getBoolean("verified")); 1.108 + default: 1.109 + throw new IllegalStateException("unrecognized state label: " + stateLabel); 1.110 + } 1.111 + } 1.112 + 1.113 + /** 1.114 + * Exactly the same as {@link fromJSONObjectV1}, except that all key pairs are DSA key pairs. 1.115 + */ 1.116 + protected static State fromJSONObjectV2(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { 1.117 + switch (stateLabel) { 1.118 + case Cohabiting: 1.119 + return new Cohabiting( 1.120 + o.getString("email"), 1.121 + o.getString("uid"), 1.122 + Utils.hex2Byte(o.getString("sessionToken")), 1.123 + Utils.hex2Byte(o.getString("kA")), 1.124 + Utils.hex2Byte(o.getString("kB")), 1.125 + keyPairFromJSONObjectV2(o.getObject("keyPair"))); 1.126 + case Married: 1.127 + return new Married( 1.128 + o.getString("email"), 1.129 + o.getString("uid"), 1.130 + Utils.hex2Byte(o.getString("sessionToken")), 1.131 + Utils.hex2Byte(o.getString("kA")), 1.132 + Utils.hex2Byte(o.getString("kB")), 1.133 + keyPairFromJSONObjectV2(o.getObject("keyPair")), 1.134 + o.getString("certificate")); 1.135 + default: 1.136 + return fromJSONObjectV1(stateLabel, o); 1.137 + } 1.138 + } 1.139 + 1.140 + protected static void logMigration(State from, State to) { 1.141 + if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) { 1.142 + return; 1.143 + } 1.144 + try { 1.145 + FxAccountConstants.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString()); 1.146 + } catch (Exception e) { 1.147 + Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e); 1.148 + } 1.149 + FxAccountConstants.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString()); 1.150 + } 1.151 + 1.152 + protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException { 1.153 + if (state == null) { 1.154 + // This should never happen, but let's be careful. 1.155 + Logger.error(LOG_TAG, "Got null state in migrateV1toV2; returning null."); 1.156 + return state; 1.157 + } 1.158 + 1.159 + Logger.info(LOG_TAG, "Migrating V1 persisted State to V2; stateLabel: " + stateLabel); 1.160 + 1.161 + // In V1, we use an RSA keyPair. In V2, we use a DSA keyPair. Only 1.162 + // Cohabiting and Married states have a persisted keyPair at all; all 1.163 + // other states need no conversion at all. 1.164 + switch (stateLabel) { 1.165 + case Cohabiting: { 1.166 + // In the Cohabiting state, we can just generate a new key pair and move on. 1.167 + final Cohabiting cohabiting = (Cohabiting) state; 1.168 + final BrowserIDKeyPair keyPair = generateKeyPair(); 1.169 + final State migrated = new Cohabiting(cohabiting.email, cohabiting.uid, cohabiting.sessionToken, cohabiting.kA, cohabiting.kB, keyPair); 1.170 + logMigration(cohabiting, migrated); 1.171 + return migrated; 1.172 + } 1.173 + case Married: { 1.174 + // In the Married state, we cannot only change the key pair: the stored 1.175 + // certificate signs the public key of the now obsolete key pair. We 1.176 + // regress to the Cohabiting state; the next time we sync, we should 1.177 + // advance back to Married. 1.178 + final Married married = (Married) state; 1.179 + final BrowserIDKeyPair keyPair = generateKeyPair(); 1.180 + final State migrated = new Cohabiting(married.email, married.uid, married.sessionToken, married.kA, married.kB, keyPair); 1.181 + logMigration(married, migrated); 1.182 + return migrated; 1.183 + } 1.184 + default: 1.185 + // Otherwise, V1 and V2 states are identical. 1.186 + return state; 1.187 + } 1.188 + } 1.189 +}