Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.fxa.authenticator;
7 import java.io.FileOutputStream;
8 import java.io.PrintStream;
9 import java.security.NoSuchAlgorithmException;
10 import java.security.spec.InvalidKeySpecException;
12 import org.mozilla.gecko.background.common.log.Logger;
13 import org.mozilla.gecko.fxa.FxAccountConstants;
14 import org.mozilla.gecko.fxa.login.State;
15 import org.mozilla.gecko.fxa.login.State.StateLabel;
16 import org.mozilla.gecko.fxa.login.StateFactory;
17 import org.mozilla.gecko.sync.ExtendedJSONObject;
18 import org.mozilla.gecko.sync.NonObjectJSONException;
19 import org.mozilla.gecko.sync.Utils;
21 import android.content.Context;
23 /**
24 * Android deletes Account objects when the Authenticator that owns the Account
25 * disappears. This happens when an App is installed to the SD card and the SD
26 * card is un-mounted or the device is rebooted.
27 * <p>
28 * We work around this by pickling the current Firefox account data every sync
29 * and unpickling when we check if Firefox accounts exist (called from Fennec).
30 * <p>
31 * Android just doesn't support installing Apps that define long-lived Services
32 * and/or own Account types onto the SD card. The documentation says not to do
33 * it. There are hordes of developers who want to do it, and have tried to
34 * register for almost every "package installation changed" broadcast intent
35 * that Android supports. They all explicitly state that the package that has
36 * changed does *not* receive the broadcast intent, thereby preventing an App
37 * from re-establishing its state.
38 * <p>
39 * <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a>
40 * <p>
41 * <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality
42 * will not work until external storage is remounted.
43 * <p>
44 * <b>Quote</b>: Your running Service will be killed and will not be restarted
45 * when external storage is remounted. You can, however, register for the
46 * ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify
47 * your application when applications installed on external storage have become
48 * available to the system again. At which time, you can restart your Service.
49 * <p>
50 * Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>!
51 * <p>
52 * See bug 768102 for more information in the context of Sync.
53 */
54 public class AccountPickler {
55 public static final String LOG_TAG = AccountPickler.class.getSimpleName();
57 public static final long PICKLE_VERSION = 2;
59 private static final String KEY_PICKLE_VERSION = "pickle_version";
60 private static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp";
62 private static final String KEY_ACCOUNT_VERSION = "account_version";
63 private static final String KEY_ACCOUNT_TYPE = "account_type";
64 private static final String KEY_EMAIL = "email";
65 private static final String KEY_PROFILE = "profile";
66 private static final String KEY_IDP_SERVER_URI = "idpServerURI";
67 private static final String KEY_TOKEN_SERVER_URI = "tokenServerURI";
68 private static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled";
70 private static final String KEY_BUNDLE = "bundle";
72 /**
73 * Remove Firefox account persisted to disk.
74 *
75 * @param context Android context.
76 * @param filename name of persisted pickle file; must not contain path separators.
77 * @return <code>true</code> if given pickle existed and was successfully deleted.
78 */
79 public static boolean deletePickle(final Context context, final String filename) {
80 return context.deleteFile(filename);
81 }
83 /**
84 * Persist Firefox account to disk as a JSON object.
85 *
86 * @param AndroidFxAccount the account to persist to disk
87 * @param filename name of file to persist to; must not contain path separators.
88 */
89 public static void pickle(final AndroidFxAccount account, final String filename) {
90 final ExtendedJSONObject o = new ExtendedJSONObject();
91 o.put(KEY_PICKLE_VERSION, Long.valueOf(PICKLE_VERSION));
92 o.put(KEY_PICKLE_TIMESTAMP, Long.valueOf(System.currentTimeMillis()));
94 o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
95 o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
96 o.put(KEY_EMAIL, account.getEmail());
97 o.put(KEY_PROFILE, account.getProfile());
98 o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI());
99 o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI());
100 o.put(KEY_IS_SYNCING_ENABLED, account.isSyncing());
102 // TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs.
104 final ExtendedJSONObject bundle = account.unbundle();
105 if (bundle == null) {
106 Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting.");
107 return;
108 }
109 o.put(KEY_BUNDLE, bundle);
111 writeToDisk(account.context, filename, o);
112 }
114 private static void writeToDisk(final Context context, final String filename,
115 final ExtendedJSONObject pickle) {
116 try {
117 final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
118 try {
119 final PrintStream ps = new PrintStream(fos);
120 try {
121 ps.print(pickle.toJSONString());
122 Logger.debug(LOG_TAG, "Persisted " + pickle.keySet().size() +
123 " account settings to " + filename + ".");
124 } finally {
125 ps.close();
126 }
127 } finally {
128 fos.close();
129 }
130 } catch (Exception e) {
131 Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename +
132 "; ignoring.", e);
133 }
134 }
136 /**
137 * Create Android account from saved JSON object. Assumes that an account does not exist.
138 *
139 * @param context
140 * Android context.
141 * @param filename
142 * name of file to read from; must not contain path separators.
143 * @return created Android account, or null on error.
144 */
145 public static AndroidFxAccount unpickle(final Context context, final String filename) {
146 final String jsonString = Utils.readFile(context, filename);
147 if (jsonString == null) {
148 Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting.");
149 return null;
150 }
152 ExtendedJSONObject json = null;
153 try {
154 json = ExtendedJSONObject.parseJSONObject(jsonString);
155 } catch (Exception e) {
156 Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e);
157 return null;
158 }
160 final UnpickleParams params;
161 try {
162 params = UnpickleParams.fromJSON(json);
163 } catch (Exception e) {
164 Logger.warn(LOG_TAG, "Got exception extracting unpickle json; aborting.", e);
165 return null;
166 }
168 final AndroidFxAccount account;
169 try {
170 account = AndroidFxAccount.addAndroidAccount(context, params.email, params.profile,
171 params.idpServerURI, params.tokenServerURI, params.state, params.accountVersion,
172 params.isSyncingEnabled, true, params.bundle);
173 } catch (Exception e) {
174 Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e);
175 return null;
176 }
178 if (account == null) {
179 Logger.warn(LOG_TAG, "Failed to add Android Account; aborting.");
180 return null;
181 }
183 Long timestamp = json.getLong(KEY_PICKLE_TIMESTAMP);
184 if (timestamp == null) {
185 Logger.warn(LOG_TAG, "Did not find timestamp in pickle file; ignoring.");
186 timestamp = Long.valueOf(-1);
187 }
189 Logger.info(LOG_TAG, "Un-pickled Android account named " + params.email + " (version " +
190 params.pickleVersion + ", pickled at " + timestamp + ").");
192 return account;
193 }
195 private static class UnpickleParams {
196 private Long pickleVersion;
198 private int accountVersion;
199 private String email;
200 private String profile;
201 private String idpServerURI;
202 private String tokenServerURI;
203 private boolean isSyncingEnabled;
205 private ExtendedJSONObject bundle;
206 private State state;
208 private UnpickleParams() {
209 }
211 private static UnpickleParams fromJSON(final ExtendedJSONObject json)
212 throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
213 final UnpickleParams params = new UnpickleParams();
214 params.pickleVersion = json.getLong(KEY_PICKLE_VERSION);
215 if (params.pickleVersion == null) {
216 throw new IllegalStateException("Pickle version not found.");
217 }
219 /*
220 * Version 1 and version 2 are identical, except version 2 throws if the
221 * internal Android Account type has changed. Version 1 used to throw in
222 * this case, but we intentionally used the pickle file to migrate across
223 * Account types, bumping the version simultaneously.
224 */
225 switch (params.pickleVersion.intValue()) {
226 case 2: {
227 // Sanity check.
228 final String accountType = json.getString(KEY_ACCOUNT_TYPE);
229 if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
230 throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + ".");
231 }
233 params.unpickleV1(json);
234 }
235 break;
237 case 1: {
238 // Warn about account type changing, but don't throw over it.
239 final String accountType = json.getString(KEY_ACCOUNT_TYPE);
240 if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
241 Logger.warn(LOG_TAG, "Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "; ignoring.");
242 }
244 params.unpickleV1(json);
245 }
246 break;
248 default:
249 throw new IllegalStateException("Unknown pickle version, " + params.pickleVersion + ".");
250 }
252 return params;
253 }
255 private void unpickleV1(final ExtendedJSONObject json)
256 throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
258 this.accountVersion = json.getIntegerSafely(KEY_ACCOUNT_VERSION);
259 this.email = json.getString(KEY_EMAIL);
260 this.profile = json.getString(KEY_PROFILE);
261 this.idpServerURI = json.getString(KEY_IDP_SERVER_URI);
262 this.tokenServerURI = json.getString(KEY_TOKEN_SERVER_URI);
263 this.isSyncingEnabled = json.getBoolean(KEY_IS_SYNCING_ENABLED);
265 this.bundle = json.getObject(KEY_BUNDLE);
266 if (bundle == null) {
267 throw new IllegalStateException("Pickle bundle is null.");
268 }
269 this.state = getState(bundle);
270 }
272 private State getState(final ExtendedJSONObject bundle) throws InvalidKeySpecException,
273 NonObjectJSONException, NoSuchAlgorithmException {
274 // TODO: Should copy-pasta BUNDLE_KEY_STATE & LABEL to this file to ensure we maintain
275 // old versions?
276 final StateLabel stateLabel = StateLabel.valueOf(
277 bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL));
278 final String stateString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE);
279 if (stateLabel == null) {
280 throw new IllegalStateException("stateLabel must not be null");
281 }
282 if (stateString == null) {
283 throw new IllegalStateException("stateString must not be null");
284 }
286 try {
287 return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
288 } catch (Exception e) {
289 throw new IllegalStateException("could not get state", e);
290 }
291 }
292 }
293 }