summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java
blob: a98f2fb2766559c4dcc46652aaccc6b043eb4910 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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;
    }
  }
}