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.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 }