mobile/android/base/fxa/authenticator/AccountPickler.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial