mobile/android/base/fxa/authenticator/AndroidFxAccount.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.

     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.UnsupportedEncodingException;
     8 import java.net.URISyntaxException;
     9 import java.security.GeneralSecurityException;
    10 import java.util.ArrayList;
    11 import java.util.Arrays;
    12 import java.util.Collections;
    13 import java.util.EnumSet;
    14 import java.util.List;
    16 import org.mozilla.gecko.background.common.GlobalConstants;
    17 import org.mozilla.gecko.background.common.log.Logger;
    18 import org.mozilla.gecko.background.fxa.FxAccountUtils;
    19 import org.mozilla.gecko.db.BrowserContract;
    20 import org.mozilla.gecko.fxa.FirefoxAccounts;
    21 import org.mozilla.gecko.fxa.FxAccountConstants;
    22 import org.mozilla.gecko.fxa.login.State;
    23 import org.mozilla.gecko.fxa.login.State.StateLabel;
    24 import org.mozilla.gecko.fxa.login.StateFactory;
    25 import org.mozilla.gecko.sync.ExtendedJSONObject;
    26 import org.mozilla.gecko.sync.Utils;
    28 import android.accounts.Account;
    29 import android.accounts.AccountManager;
    30 import android.content.ContentResolver;
    31 import android.content.Context;
    32 import android.content.Intent;
    33 import android.content.SharedPreferences;
    34 import android.os.Bundle;
    36 /**
    37  * A Firefox Account that stores its details and state as user data attached to
    38  * an Android Account instance.
    39  * <p>
    40  * Account user data is accessible only to the Android App(s) that own the
    41  * Account type. Account user data is not removed when the App's private data is
    42  * cleared.
    43  */
    44 public class AndroidFxAccount {
    45   protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
    47   public static final int CURRENT_PREFS_VERSION = 1;
    49   // When updating the account, do not forget to update AccountPickler.
    50   public static final int CURRENT_ACCOUNT_VERSION = 3;
    51   public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
    52   public static final String ACCOUNT_KEY_PROFILE = "profile";
    53   public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI";
    55   // The audience should always be a prefix of the token server URI.
    56   public static final String ACCOUNT_KEY_AUDIENCE = "audience";                 // Sync-specific.
    57   public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI";       // Sync-specific.
    58   public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
    60   public static final int CURRENT_BUNDLE_VERSION = 2;
    61   public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
    62   public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
    63   public static final String BUNDLE_KEY_STATE = "state";
    65   protected static final List<String> ANDROID_AUTHORITIES = Collections.unmodifiableList(Arrays.asList(
    66       new String[] { BrowserContract.AUTHORITY }));
    68   protected final Context context;
    69   protected final AccountManager accountManager;
    70   protected final Account account;
    72   /**
    73    * Create an Android Firefox Account instance backed by an Android Account
    74    * instance.
    75    * <p>
    76    * We expect a long-lived application context to avoid life-cycle issues that
    77    * might arise if the internally cached AccountManager instance surfaces UI.
    78    * <p>
    79    * We take care to not install any listeners or observers that might outlive
    80    * the AccountManager; and Android ensures the AccountManager doesn't outlive
    81    * the associated context.
    82    *
    83    * @param applicationContext
    84    *          to use as long-lived ambient Android context.
    85    * @param account
    86    *          Android account to use for storage.
    87    */
    88   public AndroidFxAccount(Context applicationContext, Account account) {
    89     this.context = applicationContext;
    90     this.account = account;
    91     this.accountManager = AccountManager.get(this.context);
    92   }
    94   /**
    95    * Persist the Firefox account to disk as a JSON object. Note that this is a wrapper around
    96    * {@link AccountPickler#pickle}, and is identical to calling it directly.
    97    * <p>
    98    * Note that pickling is different from bundling, which involves operations on a
    99    * {@link android.os.Bundle Bundle} object of miscellaenous data associated with the account.
   100    * See {@link #persistBundle} and {@link #unbundle} for more.
   101    */
   102   public void pickle(final String filename) {
   103     AccountPickler.pickle(this, filename);
   104   }
   106   public Account getAndroidAccount() {
   107     return this.account;
   108   }
   110   protected int getAccountVersion() {
   111     String v = accountManager.getUserData(account, ACCOUNT_KEY_ACCOUNT_VERSION);
   112     if (v == null) {
   113       return 0;         // Implicit.
   114     }
   116     try {
   117       return Integer.parseInt(v, 10);
   118     } catch (NumberFormatException ex) {
   119       return 0;
   120     }
   121   }
   123   /**
   124    * Saves the given data as the internal bundle associated with this account.
   125    * @param bundle to write to account.
   126    */
   127   protected void persistBundle(ExtendedJSONObject bundle) {
   128     accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
   129   }
   131   /**
   132    * Retrieve the internal bundle associated with this account.
   133    * @return bundle associated with account.
   134    */
   135   protected ExtendedJSONObject unbundle() {
   136     final int version = getAccountVersion();
   137     if (version < CURRENT_ACCOUNT_VERSION) {
   138       // Needs upgrade. For now, do nothing. We'd like to just put your account
   139       // into the Separated state here and have you update your credentials.
   140       return null;
   141     }
   143     if (version > CURRENT_ACCOUNT_VERSION) {
   144       // Oh dear.
   145       return null;
   146     }
   148     String bundle = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
   149     if (bundle == null) {
   150       return null;
   151     }
   152     return unbundleAccountV2(bundle);
   153   }
   155   protected String getBundleData(String key) {
   156     ExtendedJSONObject o = unbundle();
   157     if (o == null) {
   158       return null;
   159     }
   160     return o.getString(key);
   161   }
   163   protected boolean getBundleDataBoolean(String key, boolean def) {
   164     ExtendedJSONObject o = unbundle();
   165     if (o == null) {
   166       return def;
   167     }
   168     Boolean b = o.getBoolean(key);
   169     if (b == null) {
   170       return def;
   171     }
   172     return b.booleanValue();
   173   }
   175   protected byte[] getBundleDataBytes(String key) {
   176     ExtendedJSONObject o = unbundle();
   177     if (o == null) {
   178       return null;
   179     }
   180     return o.getByteArrayHex(key);
   181   }
   183   protected void updateBundleDataBytes(String key, byte[] value) {
   184     updateBundleValue(key, value == null ? null : Utils.byte2Hex(value));
   185   }
   187   protected void updateBundleValue(String key, boolean value) {
   188     ExtendedJSONObject descriptor = unbundle();
   189     if (descriptor == null) {
   190       return;
   191     }
   192     descriptor.put(key, value);
   193     persistBundle(descriptor);
   194   }
   196   protected void updateBundleValue(String key, String value) {
   197     ExtendedJSONObject descriptor = unbundle();
   198     if (descriptor == null) {
   199       return;
   200     }
   201     descriptor.put(key, value);
   202     persistBundle(descriptor);
   203   }
   205   private ExtendedJSONObject unbundleAccountV1(String bundle) {
   206     ExtendedJSONObject o;
   207     try {
   208       o = new ExtendedJSONObject(bundle);
   209     } catch (Exception e) {
   210       return null;
   211     }
   212     if (CURRENT_BUNDLE_VERSION == o.getIntegerSafely(BUNDLE_KEY_BUNDLE_VERSION)) {
   213       return o;
   214     }
   215     return null;
   216   }
   218   private ExtendedJSONObject unbundleAccountV2(String bundle) {
   219     return unbundleAccountV1(bundle);
   220   }
   222   /**
   223    * Note that if the user clears data, an account will be left pointing to a
   224    * deleted profile. Such is life.
   225    */
   226   public String getProfile() {
   227     return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE);
   228   }
   230   public String getAccountServerURI() {
   231     return accountManager.getUserData(account, ACCOUNT_KEY_IDP_SERVER);
   232   }
   234   public String getAudience() {
   235     return accountManager.getUserData(account, ACCOUNT_KEY_AUDIENCE);
   236   }
   238   public String getTokenServerURI() {
   239     return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER);
   240   }
   242   /**
   243    * This needs to return a string because of the tortured prefs access in GlobalSession.
   244    */
   245   public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
   246     String profile = getProfile();
   247     String username = account.name;
   249     if (profile == null) {
   250       throw new IllegalStateException("Missing profile. Cannot fetch prefs.");
   251     }
   253     if (username == null) {
   254       throw new IllegalStateException("Missing username. Cannot fetch prefs.");
   255     }
   257     final String tokenServerURI = getTokenServerURI();
   258     if (tokenServerURI == null) {
   259       throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
   260     }
   262     final String fxaServerURI = getAccountServerURI();
   263     if (fxaServerURI == null) {
   264       throw new IllegalStateException("No account server URI. Cannot fetch prefs.");
   265     }
   267     final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
   268     final long version = CURRENT_PREFS_VERSION;
   270     // This is unique for each syncing 'view' of the account.
   271     final String serverURLThing = fxaServerURI + "!" + tokenServerURI;
   272     return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
   273   }
   275   public SharedPreferences getSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
   276     return context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
   277   }
   279   /**
   280    * Extract a JSON dictionary of the string values associated to this account.
   281    * <p>
   282    * <b>For debugging use only!</b> The contents of this JSON object completely
   283    * determine the user's Firefox Account status and yield access to whatever
   284    * user data the device has access to.
   285    *
   286    * @return JSON-object of Strings.
   287    */
   288   public ExtendedJSONObject toJSONObject() {
   289     ExtendedJSONObject o = unbundle();
   290     o.put("email", account.name);
   291     try {
   292       o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
   293     } catch (UnsupportedEncodingException e) {
   294       // Ignore.
   295     }
   296     return o;
   297   }
   299   public static AndroidFxAccount addAndroidAccount(
   300       Context context,
   301       String email,
   302       String profile,
   303       String idpServerURI,
   304       String tokenServerURI,
   305       State state)
   306           throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
   307     return addAndroidAccount(context, email, profile, idpServerURI, tokenServerURI, state,
   308         CURRENT_ACCOUNT_VERSION, true, false, null);
   309   }
   311   public static AndroidFxAccount addAndroidAccount(
   312       Context context,
   313       String email,
   314       String profile,
   315       String idpServerURI,
   316       String tokenServerURI,
   317       State state,
   318       final int accountVersion,
   319       final boolean syncEnabled,
   320       final boolean fromPickle,
   321       ExtendedJSONObject bundle)
   322           throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
   323     if (email == null) {
   324       throw new IllegalArgumentException("email must not be null");
   325     }
   326     if (idpServerURI == null) {
   327       throw new IllegalArgumentException("idpServerURI must not be null");
   328     }
   329     if (tokenServerURI == null) {
   330       throw new IllegalArgumentException("tokenServerURI must not be null");
   331     }
   332     if (state == null) {
   333       throw new IllegalArgumentException("state must not be null");
   334     }
   336     // TODO: Add migration code.
   337     if (accountVersion != CURRENT_ACCOUNT_VERSION) {
   338       throw new IllegalStateException("Could not create account of version " + accountVersion +
   339           ". Current version is " + CURRENT_ACCOUNT_VERSION + ".");
   340     }
   342     // Android has internal restrictions that require all values in this
   343     // bundle to be strings. *sigh*
   344     Bundle userdata = new Bundle();
   345     userdata.putString(ACCOUNT_KEY_ACCOUNT_VERSION, "" + CURRENT_ACCOUNT_VERSION);
   346     userdata.putString(ACCOUNT_KEY_IDP_SERVER, idpServerURI);
   347     userdata.putString(ACCOUNT_KEY_TOKEN_SERVER, tokenServerURI);
   348     userdata.putString(ACCOUNT_KEY_AUDIENCE, FxAccountUtils.getAudienceForURL(tokenServerURI));
   349     userdata.putString(ACCOUNT_KEY_PROFILE, profile);
   351     if (bundle == null) {
   352       bundle = new ExtendedJSONObject();
   353       // TODO: How to upgrade?
   354       bundle.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
   355     }
   356     bundle.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
   357     bundle.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
   359     userdata.putString(ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
   361     Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
   362     AccountManager accountManager = AccountManager.get(context);
   363     // We don't set an Android password, because we don't want to persist the
   364     // password (or anything else as powerful as the password). Instead, we
   365     // internally manage a sessionToken with a remotely owned lifecycle.
   366     boolean added = accountManager.addAccountExplicitly(account, null, userdata);
   367     if (!added) {
   368       return null;
   369     }
   371     AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
   373     if (!fromPickle) {
   374       fxAccount.clearSyncPrefs();
   375     }
   377     if (syncEnabled) {
   378       fxAccount.enableSyncing();
   379     } else {
   380       fxAccount.disableSyncing();
   381     }
   383     return fxAccount;
   384   }
   386   public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
   387     getSyncPrefs().edit().clear().commit();
   388   }
   390   public static Iterable<String> getAndroidAuthorities() {
   391     return ANDROID_AUTHORITIES;
   392   }
   394   /**
   395    * Return true if the underlying Android account is currently set to sync automatically.
   396    * <p>
   397    * This is, confusingly, not the same thing as "being syncable": that refers
   398    * to whether this account can be synced, ever; this refers to whether Android
   399    * will try to sync the account at appropriate times.
   400    *
   401    * @return true if the account is set to sync automatically.
   402    */
   403   public boolean isSyncing() {
   404     boolean isSyncEnabled = true;
   405     for (String authority : getAndroidAuthorities()) {
   406       isSyncEnabled &= ContentResolver.getSyncAutomatically(account, authority);
   407     }
   408     return isSyncEnabled;
   409   }
   411   public void enableSyncing() {
   412     Logger.info(LOG_TAG, "Enabling sync for account named like " + getObfuscatedEmail());
   413     for (String authority : getAndroidAuthorities()) {
   414       ContentResolver.setSyncAutomatically(account, authority, true);
   415       ContentResolver.setIsSyncable(account, authority, 1);
   416     }
   417   }
   419   public void disableSyncing() {
   420     Logger.info(LOG_TAG, "Disabling sync for account named like " + getObfuscatedEmail());
   421     for (String authority : getAndroidAuthorities()) {
   422       ContentResolver.setSyncAutomatically(account, authority, false);
   423     }
   424   }
   426   /**
   427    * Is a sync currently in progress?
   428    *
   429    * @return true if Android is currently syncing the underlying Android Account.
   430    */
   431   public boolean isCurrentlySyncing() {
   432     boolean active = false;
   433     for (String authority : AndroidFxAccount.getAndroidAuthorities()) {
   434       active |= ContentResolver.isSyncActive(account, authority);
   435     }
   436     return active;
   437   }
   439   /**
   440    * Request a sync.  See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}.
   441    */
   442   public void requestSync() {
   443     requestSync(FirefoxAccounts.SOON, null, null);
   444   }
   446   /**
   447    * Request a sync.  See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}.
   448    *
   449    * @param syncHints to pass to sync.
   450    */
   451   public void requestSync(EnumSet<FirefoxAccounts.SyncHint> syncHints) {
   452     requestSync(syncHints, null, null);
   453   }
   455   /**
   456    * Request a sync.  See {@link FirefoxAccounts#requestSync(Account, EnumSet, String[], String[])}.
   457    *
   458    * @param syncHints to pass to sync.
   459    * @param stagesToSync stage names to sync.
   460    * @param stagesToSkip stage names to skip.
   461    */
   462   public void requestSync(EnumSet<FirefoxAccounts.SyncHint> syncHints, String[] stagesToSync, String[] stagesToSkip) {
   463     FirefoxAccounts.requestSync(getAndroidAccount(), syncHints, stagesToSync, stagesToSkip);
   464   }
   466   public synchronized void setState(State state) {
   467     if (state == null) {
   468       throw new IllegalArgumentException("state must not be null");
   469     }
   470     Logger.info(LOG_TAG, "Moving account named like " + getObfuscatedEmail() +
   471         " to state " + state.getStateLabel().toString());
   472     updateBundleValue(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
   473     updateBundleValue(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
   474   }
   476   public synchronized State getState() {
   477     String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL);
   478     String stateString = getBundleData(BUNDLE_KEY_STATE);
   479     if (stateLabelString == null) {
   480       throw new IllegalStateException("stateLabelString must not be null");
   481     }
   482     if (stateString == null) {
   483       throw new IllegalStateException("stateString must not be null");
   484     }
   486     try {
   487       StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
   488       return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
   489     } catch (Exception e) {
   490       throw new IllegalStateException("could not get state", e);
   491     }
   492   }
   494   /**
   495    * <b>For debugging only!</b>
   496    */
   497   public void dump() {
   498     if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
   499       return;
   500     }
   501     ExtendedJSONObject o = toJSONObject();
   502     ArrayList<String> list = new ArrayList<String>(o.keySet());
   503     Collections.sort(list);
   504     for (String key : list) {
   505       FxAccountConstants.pii(LOG_TAG, key + ": " + o.get(key));
   506     }
   507   }
   509   /**
   510    * Return the Firefox Account's local email address.
   511    * <p>
   512    * It is important to note that this is the local email address, and not
   513    * necessarily the normalized remote email address that the server expects.
   514    *
   515    * @return local email address.
   516    */
   517   public String getEmail() {
   518     return account.name;
   519   }
   521   /**
   522    * Return the Firefox Account's local email address, obfuscated.
   523    * <p>
   524    * Use this when logging.
   525    *
   526    * @return local email address, obfuscated.
   527    */
   528   public String getObfuscatedEmail() {
   529     return Utils.obfuscateEmail(account.name);
   530   }
   532   /**
   533    * Create an intent announcing that a Firefox account will be deleted.
   534    *
   535    * @param context
   536    *          Android context.
   537    * @param account
   538    *          Android account being removed.
   539    * @return <code>Intent</code> to broadcast.
   540    */
   541   public static Intent makeDeletedAccountIntent(final Context context, final Account account) {
   542     final Intent intent = new Intent(FxAccountConstants.ACCOUNT_DELETED_ACTION);
   544     intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY,
   545         Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION));
   546     intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name);
   547     return intent;
   548   }
   549 }

mercurial