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

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     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 }

mercurial