diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java')
-rw-r--r-- | mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java new file mode 100644 index 000000000..3f2c5620d --- /dev/null +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java @@ -0,0 +1,362 @@ +/* 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.authenticator; + +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.fxa.FxAccountConstants; +import org.mozilla.gecko.fxa.login.State; +import org.mozilla.gecko.fxa.login.State.StateLabel; +import org.mozilla.gecko.fxa.login.StateFactory; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.Utils; + +import android.content.Context; + +/** + * Android deletes Account objects when the Authenticator that owns the Account + * disappears. This happens when an App is installed to the SD card and the SD + * card is un-mounted or the device is rebooted. + * <p> + * We work around this by pickling the current Firefox account data every sync + * and unpickling when we check if Firefox accounts exist (called from Fennec). + * <p> + * Android just doesn't support installing Apps that define long-lived Services + * and/or own Account types onto the SD card. The documentation says not to do + * it. There are hordes of developers who want to do it, and have tried to + * register for almost every "package installation changed" broadcast intent + * that Android supports. They all explicitly state that the package that has + * changed does *not* receive the broadcast intent, thereby preventing an App + * from re-establishing its state. + * <p> + * <a href="http://developer.android.com/guide/topics/data/install-location.html">Reference.</a> + * <p> + * <b>Quote</b>: Your AbstractThreadedSyncAdapter and all its sync functionality + * will not work until external storage is remounted. + * <p> + * <b>Quote</b>: Your running Service will be killed and will not be restarted + * when external storage is remounted. You can, however, register for the + * ACTION_EXTERNAL_APPLICATIONS_AVAILABLE broadcast Intent, which will notify + * your application when applications installed on external storage have become + * available to the system again. At which time, you can restart your Service. + * <p> + * Problem: <a href="http://code.google.com/p/android/issues/detail?id=8485">that intent doesn't work</a>! + * <p> + * See bug 768102 for more information in the context of Sync. + */ +public class AccountPickler { + public static final String LOG_TAG = AccountPickler.class.getSimpleName(); + + public static final long PICKLE_VERSION = 3; + + public static final String KEY_PICKLE_VERSION = "pickle_version"; + public static final String KEY_PICKLE_TIMESTAMP = "pickle_timestamp"; + + public static final String KEY_ACCOUNT_VERSION = "account_version"; + public static final String KEY_ACCOUNT_TYPE = "account_type"; + public static final String KEY_EMAIL = "email"; + public static final String KEY_PROFILE = "profile"; + public static final String KEY_IDP_SERVER_URI = "idpServerURI"; + public static final String KEY_TOKEN_SERVER_URI = "tokenServerURI"; + public static final String KEY_PROFILE_SERVER_URI = "profileServerURI"; + + public static final String KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = "authoritiesToSyncAutomaticallyMap"; + + // Deprecated, but maintained for migration purposes. + public static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled"; + + public static final String KEY_BUNDLE = "bundle"; + + /** + * Remove Firefox account persisted to disk. + * This operation is synchronized to avoid race condition while deleting the account. + * + * @param context Android context. + * @param filename name of persisted pickle file; must not contain path separators. + * @return <code>true</code> if given pickle existed and was successfully deleted. + */ + public synchronized static boolean deletePickle(final Context context, final String filename) { + return context.deleteFile(filename); + } + + public static ExtendedJSONObject toJSON(final AndroidFxAccount account, final long now) { + final ExtendedJSONObject o = new ExtendedJSONObject(); + o.put(KEY_PICKLE_VERSION, PICKLE_VERSION); + o.put(KEY_PICKLE_TIMESTAMP, now); + + o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION); + o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE); + o.put(KEY_EMAIL, account.getEmail()); + o.put(KEY_PROFILE, account.getProfile()); + o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI()); + o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI()); + o.put(KEY_PROFILE_SERVER_URI, account.getProfileServerURI()); + + final ExtendedJSONObject p = new ExtendedJSONObject(); + for (Entry<String, Boolean> pair : account.getAuthoritiesToSyncAutomaticallyMap().entrySet()) { + p.put(pair.getKey(), pair.getValue()); + } + o.put(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP, p); + + // TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs. + + final ExtendedJSONObject bundle = account.unbundle(); + if (bundle == null) { + Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting."); + return null; + } + o.put(KEY_BUNDLE, bundle); + + return o; + } + + /** + * Persist Firefox account to disk as a JSON object. + * This operation is synchronized to avoid race condition while deleting the account. + * + * @param account the AndroidFxAccount to persist to disk + * @param filename name of file to persist to; must not contain path separators. + */ + public synchronized static void pickle(final AndroidFxAccount account, final String filename) { + final ExtendedJSONObject o = toJSON(account, System.currentTimeMillis()); + writeToDisk(account.context, filename, o); + } + + private static void writeToDisk(final Context context, final String filename, + final ExtendedJSONObject pickle) { + try { + final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE); + try { + final PrintStream ps = new PrintStream(fos); + try { + ps.print(pickle.toJSONString()); + Logger.debug(LOG_TAG, "Persisted " + pickle.keySet().size() + + " account settings to " + filename + "."); + } finally { + ps.close(); + } + } finally { + fos.close(); + } + } catch (Exception e) { + Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename + + "; ignoring.", e); + } + } + + /** + * Create Android account from saved JSON object. Assumes that an account does not exist. + * This operation is synchronized to avoid race condition while deleting the account. + * + * @param context + * Android context. + * @param filename + * name of file to read from; must not contain path separators. + * @return created Android account, or null on error. + */ + public synchronized static AndroidFxAccount unpickle(final Context context, final String filename) { + final String jsonString = Utils.readFile(context, filename); + if (jsonString == null) { + Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting."); + return null; + } + + ExtendedJSONObject json = null; + try { + json = new ExtendedJSONObject(jsonString); + } catch (Exception e) { + Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e); + return null; + } + + final UnpickleParams params; + try { + params = UnpickleParams.fromJSON(json); + } catch (Exception e) { + Logger.warn(LOG_TAG, "Got exception extracting unpickle json; aborting.", e); + return null; + } + + final AndroidFxAccount account; + try { + account = AndroidFxAccount.addAndroidAccount(context, params.email, params.profile, + params.authServerURI, params.tokenServerURI, params.profileServerURI, params.state, + params.authoritiesToSyncAutomaticallyMap, + params.accountVersion, + true, params.bundle); + } catch (Exception e) { + Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e); + return null; + } + + if (account == null) { + Logger.warn(LOG_TAG, "Failed to add Android Account; aborting."); + return null; + } + + Long timestamp = json.getLong(KEY_PICKLE_TIMESTAMP); + if (timestamp == null) { + Logger.warn(LOG_TAG, "Did not find timestamp in pickle file; ignoring."); + timestamp = -1L; + } + + Logger.info(LOG_TAG, "Un-pickled Android account named " + params.email + " (version " + + params.pickleVersion + ", pickled at " + timestamp + ")."); + + return account; + } + + private static class UnpickleParams { + private Long pickleVersion; + + private int accountVersion; + private String email; + private String profile; + private String authServerURI; + private String tokenServerURI; + private String profileServerURI; + private final Map<String, Boolean> authoritiesToSyncAutomaticallyMap = new HashMap<>(); + + private ExtendedJSONObject bundle; + private State state; + + private UnpickleParams() { + } + + private static UnpickleParams fromJSON(final ExtendedJSONObject json) + throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { + final UnpickleParams params = new UnpickleParams(); + params.pickleVersion = json.getLong(KEY_PICKLE_VERSION); + if (params.pickleVersion == null) { + throw new IllegalStateException("Pickle version not found."); + } + + /* + * Version 1 and version 2 are identical, except version 2 throws if the + * internal Android Account type has changed. Version 1 used to throw in + * this case, but we intentionally used the pickle file to migrate across + * Account types, bumping the version simultaneously. + * + * Version 3 replaces "isSyncEnabled" with a map (String -> Boolean) + * associating Android authorities to whether or not they are configured + * to sync automatically. + */ + switch (params.pickleVersion.intValue()) { + case 3: { + // Sanity check. + final String accountType = json.getString(KEY_ACCOUNT_TYPE); + if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) { + throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "."); + } + + params.unpickleV3(json); + } + break; + + case 2: { + // Sanity check. + final String accountType = json.getString(KEY_ACCOUNT_TYPE); + if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) { + throw new IllegalStateException("Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "."); + } + + params.unpickleV1(json); + } + break; + + case 1: { + // Warn about account type changing, but don't throw over it. + final String accountType = json.getString(KEY_ACCOUNT_TYPE); + if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) { + Logger.warn(LOG_TAG, "Account type has changed from " + accountType + " to " + FxAccountConstants.ACCOUNT_TYPE + "; ignoring."); + } + + params.unpickleV1(json); + } + break; + + default: + throw new IllegalStateException("Unknown pickle version, " + params.pickleVersion + "."); + } + + return params; + } + + private void unpickleV1(final ExtendedJSONObject json) + throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException { + + this.accountVersion = json.getIntegerSafely(KEY_ACCOUNT_VERSION); + this.email = json.getString(KEY_EMAIL); + this.profile = json.getString(KEY_PROFILE); + this.authServerURI = json.getString(KEY_IDP_SERVER_URI); + this.tokenServerURI = json.getString(KEY_TOKEN_SERVER_URI); + this.profileServerURI = json.getString(KEY_PROFILE_SERVER_URI); + + // Fallback to default value when profile server URI was not pickled. + if (this.profileServerURI == null) { + this.profileServerURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(this.authServerURI) + ? FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT + : FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT; + } + + // We get the default value for everything except syncing browser data. + this.authoritiesToSyncAutomaticallyMap.put(BrowserContract.AUTHORITY, json.getBoolean(KEY_IS_SYNCING_ENABLED)); + + this.bundle = json.getObject(KEY_BUNDLE); + if (bundle == null) { + throw new IllegalStateException("Pickle bundle is null."); + } + this.state = getState(bundle); + } + + private void unpickleV3(final ExtendedJSONObject json) + throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException { + // We'll overwrite the extracted sync automatically map. + unpickleV1(json); + + // Extract the map of authorities to sync automatically. + authoritiesToSyncAutomaticallyMap.clear(); + final ExtendedJSONObject o = json.getObject(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP); + if (o == null) { + return; + } + for (String key : o.keySet()) { + final Boolean enabled = o.getBoolean(key); + if (enabled != null) { + authoritiesToSyncAutomaticallyMap.put(key, enabled); + } + } + } + + private State getState(final ExtendedJSONObject bundle) throws InvalidKeySpecException, + NonObjectJSONException, NoSuchAlgorithmException { + // TODO: Should copy-pasta BUNDLE_KEY_STATE & LABEL to this file to ensure we maintain + // old versions? + final StateLabel stateLabelString = StateLabel.valueOf( + bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL)); + final String stateString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE); + if (stateLabelString == null || stateString == null) { + throw new IllegalStateException("stateLabel and stateString must not be null, but: " + + "(stateLabel == null) = " + (stateLabelString == null) + + " and (stateString == null) = " + (stateString == null)); + } + + try { + return StateFactory.fromJSONObject(stateLabelString, new ExtendedJSONObject(stateString)); + } catch (Exception e) { + throw new IllegalStateException("could not get state", e); + } + } + } +} |