Files
app_Settings/src/com/android/settings/accounts/ManageAccountsSettings.java
Carlos Valdivia 92f7615502 SECURITY: Prevent preference intents from exploiting settings.
Settings has super powers because it shares the system uid. We prevent
those powers from being exploited by malicious authenticators by
checking the intents those authenticators provide.

We will only launch intents that resolve to Activities that are exported=true
or owned by the same UID as the authenticator.

Bug: 14408627
Change-Id: Ia179df8561d29745767dac0f3c97eb0f68083f59
2014-09-09 11:17:19 -07:00

533 lines
23 KiB
Java

/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.accounts;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.app.ActionBar;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.SyncAdapterType;
import android.content.SyncInfo;
import android.content.SyncStatusInfo;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceScreen;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import com.android.settings.AccountPreference;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.location.LocationSettings;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import static android.content.Intent.EXTRA_USER;
/** Manages settings for Google Account. */
public class ManageAccountsSettings extends AccountPreferenceBase
implements AuthenticatorHelper.OnAccountsUpdateListener {
private static final String ACCOUNT_KEY = "account"; // to pass to auth settings
public static final String KEY_ACCOUNT_TYPE = "account_type";
public static final String KEY_ACCOUNT_LABEL = "account_label";
// Action name for the broadcast intent when the Google account preferences page is launching
// the location settings.
private static final String LAUNCHING_LOCATION_SETTINGS =
"com.android.settings.accounts.LAUNCHING_LOCATION_SETTINGS";
private static final int MENU_SYNC_NOW_ID = Menu.FIRST;
private static final int MENU_SYNC_CANCEL_ID = Menu.FIRST + 1;
private static final int REQUEST_SHOW_SYNC_SETTINGS = 1;
private String[] mAuthorities;
private TextView mErrorInfoView;
// If an account type is set, then show only accounts of that type
private String mAccountType;
// Temporary hack, to deal with backward compatibility
private Account mFirstAccount;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Bundle args = getArguments();
if (args != null && args.containsKey(KEY_ACCOUNT_TYPE)) {
mAccountType = args.getString(KEY_ACCOUNT_TYPE);
}
addPreferencesFromResource(R.xml.manage_accounts_settings);
setHasOptionsMenu(true);
}
@Override
public void onStart() {
super.onStart();
mAuthenticatorHelper.listenToAccountUpdates();
updateAuthDescriptions();
showAccountsIfNeeded();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.manage_accounts_screen, container, false);
final ListView list = (ListView) view.findViewById(android.R.id.list);
Utils.prepareCustomPreferencesList(container, view, list, false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final Activity activity = getActivity();
final View view = getView();
mErrorInfoView = (TextView)view.findViewById(R.id.sync_settings_error_info);
mErrorInfoView.setVisibility(View.GONE);
mAuthorities = activity.getIntent().getStringArrayExtra(AUTHORITIES_FILTER_KEY);
Bundle args = getArguments();
if (args != null && args.containsKey(KEY_ACCOUNT_LABEL)) {
getActivity().setTitle(args.getString(KEY_ACCOUNT_LABEL));
}
}
@Override
public void onStop() {
super.onStop();
final Activity activity = getActivity();
mAuthenticatorHelper.stopListeningToAccountUpdates();
activity.getActionBar().setDisplayOptions(0, ActionBar.DISPLAY_SHOW_CUSTOM);
activity.getActionBar().setCustomView(null);
}
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferences, Preference preference) {
if (preference instanceof AccountPreference) {
startAccountSettings((AccountPreference) preference);
} else {
return false;
}
return true;
}
private void startAccountSettings(AccountPreference acctPref) {
Bundle args = new Bundle();
args.putParcelable(AccountSyncSettings.ACCOUNT_KEY, acctPref.getAccount());
args.putParcelable(EXTRA_USER, mUserHandle);
((SettingsActivity) getActivity()).startPreferencePanel(
AccountSyncSettings.class.getCanonicalName(), args,
R.string.account_sync_settings_title, acctPref.getAccount().name,
this, REQUEST_SHOW_SYNC_SETTINGS);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
MenuItem syncNow = menu.add(0, MENU_SYNC_NOW_ID, 0,
getString(R.string.sync_menu_sync_now))
.setIcon(R.drawable.ic_menu_refresh_holo_dark);
MenuItem syncCancel = menu.add(0, MENU_SYNC_CANCEL_ID, 0,
getString(R.string.sync_menu_sync_cancel))
.setIcon(com.android.internal.R.drawable.ic_menu_close_clear_cancel);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
boolean syncActive = ContentResolver.getCurrentSyncsAsUser(
mUserHandle.getIdentifier()).isEmpty();
menu.findItem(MENU_SYNC_NOW_ID).setVisible(!syncActive && mFirstAccount != null);
menu.findItem(MENU_SYNC_CANCEL_ID).setVisible(syncActive && mFirstAccount != null);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_SYNC_NOW_ID:
requestOrCancelSyncForAccounts(true);
return true;
case MENU_SYNC_CANCEL_ID:
requestOrCancelSyncForAccounts(false);
return true;
}
return super.onOptionsItemSelected(item);
}
private void requestOrCancelSyncForAccounts(boolean sync) {
final int userId = mUserHandle.getIdentifier();
SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
int count = getPreferenceScreen().getPreferenceCount();
// For each account
for (int i = 0; i < count; i++) {
Preference pref = getPreferenceScreen().getPreference(i);
if (pref instanceof AccountPreference) {
Account account = ((AccountPreference) pref).getAccount();
// For all available sync authorities, sync those that are enabled for the account
for (int j = 0; j < syncAdapters.length; j++) {
SyncAdapterType sa = syncAdapters[j];
if (syncAdapters[j].accountType.equals(mAccountType)
&& ContentResolver.getSyncAutomaticallyAsUser(account, sa.authority,
userId)) {
if (sync) {
ContentResolver.requestSyncAsUser(account, sa.authority, userId,
extras);
} else {
ContentResolver.cancelSyncAsUser(account, sa.authority, userId);
}
}
}
}
}
}
@Override
protected void onSyncStateUpdated() {
showSyncState();
}
private void showSyncState() {
// Catch any delayed delivery of update messages
if (getActivity() == null) return;
final int userId = mUserHandle.getIdentifier();
// iterate over all the preferences, setting the state properly for each
List<SyncInfo> currentSyncs = ContentResolver.getCurrentSyncsAsUser(userId);
boolean anySyncFailed = false; // true if sync on any account failed
Date date = new Date();
// only track userfacing sync adapters when deciding if account is synced or not
final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId);
HashSet<String> userFacing = new HashSet<String>();
for (int k = 0, n = syncAdapters.length; k < n; k++) {
final SyncAdapterType sa = syncAdapters[k];
if (sa.isUserVisible()) {
userFacing.add(sa.authority);
}
}
for (int i = 0, count = getPreferenceScreen().getPreferenceCount(); i < count; i++) {
Preference pref = getPreferenceScreen().getPreference(i);
if (! (pref instanceof AccountPreference)) {
continue;
}
AccountPreference accountPref = (AccountPreference) pref;
Account account = accountPref.getAccount();
int syncCount = 0;
long lastSuccessTime = 0;
boolean syncIsFailing = false;
final ArrayList<String> authorities = accountPref.getAuthorities();
boolean syncingNow = false;
if (authorities != null) {
for (String authority : authorities) {
SyncStatusInfo status = ContentResolver.getSyncStatusAsUser(account, authority,
userId);
boolean syncEnabled = isSyncEnabled(userId, account, authority);
boolean authorityIsPending = ContentResolver.isSyncPending(account, authority);
boolean activelySyncing = isSyncing(currentSyncs, account, authority);
boolean lastSyncFailed = status != null
&& syncEnabled
&& status.lastFailureTime != 0
&& status.getLastFailureMesgAsInt(0)
!= ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS;
if (lastSyncFailed && !activelySyncing && !authorityIsPending) {
syncIsFailing = true;
anySyncFailed = true;
}
syncingNow |= activelySyncing;
if (status != null && lastSuccessTime < status.lastSuccessTime) {
lastSuccessTime = status.lastSuccessTime;
}
syncCount += syncEnabled && userFacing.contains(authority) ? 1 : 0;
}
} else {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "no syncadapters found for " + account);
}
}
if (syncIsFailing) {
accountPref.setSyncStatus(AccountPreference.SYNC_ERROR, true);
} else if (syncCount == 0) {
accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true);
} else if (syncCount > 0) {
if (syncingNow) {
accountPref.setSyncStatus(AccountPreference.SYNC_IN_PROGRESS, true);
} else {
accountPref.setSyncStatus(AccountPreference.SYNC_ENABLED, true);
if (lastSuccessTime > 0) {
accountPref.setSyncStatus(AccountPreference.SYNC_ENABLED, false);
date.setTime(lastSuccessTime);
final String timeString = formatSyncDate(date);
accountPref.setSummary(getResources().getString(
R.string.last_synced, timeString));
}
}
} else {
accountPref.setSyncStatus(AccountPreference.SYNC_DISABLED, true);
}
}
mErrorInfoView.setVisibility(anySyncFailed ? View.VISIBLE : View.GONE);
}
private boolean isSyncing(List<SyncInfo> currentSyncs, Account account, String authority) {
final int count = currentSyncs.size();
for (int i = 0; i < count; i++) {
SyncInfo syncInfo = currentSyncs.get(i);
if (syncInfo.account.equals(account) && syncInfo.authority.equals(authority)) {
return true;
}
}
return false;
}
private boolean isSyncEnabled(int userId, Account account, String authority) {
return ContentResolver.getSyncAutomaticallyAsUser(account, authority, userId)
&& ContentResolver.getMasterSyncAutomaticallyAsUser(userId)
&& (ContentResolver.getIsSyncableAsUser(account, authority, userId) > 0);
}
@Override
public void onAccountsUpdate(UserHandle userHandle) {
showAccountsIfNeeded();
onSyncStateUpdated();
}
private void showAccountsIfNeeded() {
if (getActivity() == null) return;
Account[] accounts = AccountManager.get(getActivity()).getAccountsAsUser(
mUserHandle.getIdentifier());
getPreferenceScreen().removeAll();
mFirstAccount = null;
addPreferencesFromResource(R.xml.manage_accounts_settings);
for (int i = 0, n = accounts.length; i < n; i++) {
final Account account = accounts[i];
// If an account type is specified for this screen, skip other types
if (mAccountType != null && !account.type.equals(mAccountType)) continue;
final ArrayList<String> auths = getAuthoritiesForAccountType(account.type);
boolean showAccount = true;
if (mAuthorities != null && auths != null) {
showAccount = false;
for (String requestedAuthority : mAuthorities) {
if (auths.contains(requestedAuthority)) {
showAccount = true;
break;
}
}
}
if (showAccount) {
final Drawable icon = getDrawableForType(account.type);
final AccountPreference preference =
new AccountPreference(getActivity(), account, icon, auths, false);
getPreferenceScreen().addPreference(preference);
if (mFirstAccount == null) {
mFirstAccount = account;
getActivity().invalidateOptionsMenu();
}
}
}
if (mAccountType != null && mFirstAccount != null) {
addAuthenticatorSettings();
} else {
// There's no account, reset to top-level of settings
Intent settingsTop = new Intent(android.provider.Settings.ACTION_SETTINGS);
settingsTop.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
getActivity().startActivity(settingsTop);
}
}
private void addAuthenticatorSettings() {
PreferenceScreen prefs = addPreferencesForType(mAccountType, getPreferenceScreen());
if (prefs != null) {
updatePreferenceIntents(prefs);
}
}
/** Listens to a preference click event and starts a fragment */
private class FragmentStarter
implements Preference.OnPreferenceClickListener {
private final String mClass;
private final int mTitleRes;
/**
* @param className the class name of the fragment to be started.
* @param title the title resource id of the started preference panel.
*/
public FragmentStarter(String className, int title) {
mClass = className;
mTitleRes = title;
}
@Override
public boolean onPreferenceClick(Preference preference) {
((SettingsActivity) getActivity()).startPreferencePanel(
mClass, null, mTitleRes, null, null, 0);
// Hack: announce that the Google account preferences page is launching the location
// settings
if (mClass.equals(LocationSettings.class.getName())) {
Intent intent = new Intent(LAUNCHING_LOCATION_SETTINGS);
getActivity().sendBroadcast(
intent, android.Manifest.permission.WRITE_SECURE_SETTINGS);
}
return true;
}
}
/**
* Filters through the preference list provided by GoogleLoginService.
*
* This method removes all the invalid intent from the list, adds account name as extra into the
* intent, and hack the location settings to start it as a fragment.
*/
private void updatePreferenceIntents(PreferenceScreen prefs) {
final PackageManager pm = getActivity().getPackageManager();
for (int i = 0; i < prefs.getPreferenceCount();) {
Preference pref = prefs.getPreference(i);
Intent intent = pref.getIntent();
if (intent != null) {
// Hack. Launch "Location" as fragment instead of as activity.
//
// When "Location" is launched as activity via Intent, there's no "Up" button at the
// top left, and if there's another running instance of "Location" activity, the
// back stack would usually point to some other place so the user won't be able to
// go back to the previous page by "back" key. Using fragment is a much easier
// solution to those problems.
//
// If we set Intent to null and assign a fragment to the PreferenceScreen item here,
// in order to make it work as expected, we still need to modify the container
// PreferenceActivity, override onPreferenceStartFragment() and call
// startPreferencePanel() there. In order to inject the title string there, more
// dirty further hack is still needed. It's much easier and cleaner to listen to
// preference click event here directly.
if (intent.getAction().equals(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)) {
// The OnPreferenceClickListener overrides the click event completely. No intent
// will get fired.
pref.setOnPreferenceClickListener(new FragmentStarter(
LocationSettings.class.getName(),
R.string.location_settings_title));
} else {
ResolveInfo ri = pm.resolveActivityAsUser(intent,
PackageManager.MATCH_DEFAULT_ONLY, mUserHandle.getIdentifier());
if (ri == null) {
prefs.removePreference(pref);
continue;
} else {
intent.putExtra(ACCOUNT_KEY, mFirstAccount);
intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
pref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent prefIntent = preference.getIntent();
/*
* Check the intent to see if it resolves to a exported=false
* activity that doesn't share a uid with the authenticator.
*
* Otherwise the intent is considered unsafe in that it will be
* exploiting the fact that settings has system privileges.
*/
if (isSafeIntent(pm, prefIntent)) {
getActivity().startActivityAsUser(prefIntent, mUserHandle);
} else {
Log.e(TAG,
"Refusing to launch authenticator intent because"
+ "it exploits Settings permissions: "
+ prefIntent);
}
return true;
}
});
}
}
}
i++;
}
}
/**
* Determines if the supplied Intent is safe. A safe intent is one that is
* will launch a exported=true activity or owned by the same uid as the
* authenticator supplying the intent.
*/
private boolean isSafeIntent(PackageManager pm, Intent intent) {
AuthenticatorDescription authDesc =
mAuthenticatorHelper.getAccountTypeDescription(mAccountType);
ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
if (resolveInfo == null) {
return false;
}
ActivityInfo resolvedActivityInfo = resolveInfo.activityInfo;
ApplicationInfo resolvedAppInfo = resolvedActivityInfo.applicationInfo;
try {
ApplicationInfo authenticatorAppInf = pm.getApplicationInfo(authDesc.packageName, 0);
return resolvedActivityInfo.exported
|| resolvedAppInfo.uid == authenticatorAppInf.uid;
} catch (NameNotFoundException e) {
Log.e(TAG,
"Intent considered unsafe due to exception.",
e);
return false;
}
}
@Override
protected void onAuthDescriptionsUpdated() {
// Update account icons for all account preference items
for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
Preference pref = getPreferenceScreen().getPreference(i);
if (pref instanceof AccountPreference) {
AccountPreference accPref = (AccountPreference) pref;
accPref.setSummary(getLabelForType(accPref.getAccount().type));
}
}
}
}