DO NOT MERGE Workaround for KeyguardLock problem

* The device policies that enforce the use of a device PIN or password
  can be sidestepped by apps that implement KeyguardManager.KeyguardLock
* This renders the policies unuseable
* To prevent this, the email app now scans for any packages holding the
  DISABLE_KEYGUARD permission.  The existence of any non-system app
  with this permission will put all security-enabled EAS accounts into
  a security hold, and post a dialog describing the problem.
* The user must uninstall any such app(s) in order to sync their EAS data.

Bug: 2737842
Change-Id: I4c96d76b12d9242b5c755dd60d7578a825fae597
This commit is contained in:
Andrew Stadler 2010-06-04 11:10:03 -07:00
parent 27e75533e9
commit 3ee0cad5f5
7 changed files with 193 additions and 15 deletions

View File

@ -235,6 +235,13 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.DEVICE_STORAGE_OK" /> <action android:name="android.intent.action.DEVICE_STORAGE_OK" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_CHANGED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver> </receiver>
<!-- Support for DeviceAdmin / DevicePolicyManager. See SecurityPolicy class for impl. --> <!-- Support for DeviceAdmin / DevicePolicyManager. See SecurityPolicy class for impl. -->

View File

@ -681,6 +681,12 @@
<string name="device_admin_label">Email</string> <string name="device_admin_label">Email</string>
<!-- Long-form description of the DeviceAdmin (2nd line in settings & in user conf. screen) --> <!-- Long-form description of the DeviceAdmin (2nd line in settings & in user conf. screen) -->
<string name="device_admin_description">Enables server-specified security policies</string> <string name="device_admin_description">Enables server-specified security policies</string>
<!-- Title of keyguard-lock-disabled dialog box -->
<string name="keyguard_disabled_dlg_title">Screen lock disabled</string>
<!-- Message of keyguard-lock-disabled dialog box -->
<string name="keyguard_disabled_dlg_message_fmt">This account cannot be used while the
application \"<xliff:g id="application">%s</xliff:g>\" is installed, because it
interferes with the operation of the screen lock.</string>
<!-- Notification message in notifications window when calendar sync is <!-- Notification message in notifications window when calendar sync is
automatically enabled for pre-existing Exchange accounts on upgrade --> automatically enabled for pre-existing Exchange accounts on upgrade -->

View File

@ -250,6 +250,12 @@ public class Email extends Application {
*/ */
MailService.actionReschedule(context); MailService.actionReschedule(context);
} }
// If enabling, clear the cached test for keyguard-disabling apps; This is because
// we may have missed package install/replace/remove broadcasts while we had no accounts
// and the BootReceiver was disabled.
if (enabled) {
SecurityPolicy.getInstance(context.getApplicationContext()).invalidateKeyguardCache();
}
} }
@Override @Override

View File

@ -33,11 +33,16 @@ import android.content.ContentUris;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor; import android.database.Cursor;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import java.util.List;
/** /**
* Utility functions to support reading and writing security policies, and handshaking the device * Utility functions to support reading and writing security policies, and handshaking the device
* into and out of various security states. * into and out of various security states.
@ -49,6 +54,10 @@ public class SecurityPolicy {
private DevicePolicyManager mDPM; private DevicePolicyManager mDPM;
private ComponentName mAdminName; private ComponentName mAdminName;
private PolicySet mAggregatePolicy; private PolicySet mAggregatePolicy;
// false = unknown, true = mKeyguardDisablePackageName is valid (or null)
private boolean mKeyguardDisableChecked;
// null = no apps disabling, non-null = one or more apps disabling
private String mKeyguardDisablePackageName;
/* package */ static final PolicySet NO_POLICY_SET = /* package */ static final PolicySet NO_POLICY_SET =
new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false); new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false);
@ -101,6 +110,7 @@ public class SecurityPolicy {
mDPM = null; mDPM = null;
mAdminName = new ComponentName(context, PolicyAdmin.class); mAdminName = new ComponentName(context, PolicyAdmin.class);
mAggregatePolicy = null; mAggregatePolicy = null;
mKeyguardDisableChecked = false;
} }
/** /**
@ -265,6 +275,13 @@ public class SecurityPolicy {
} }
} }
if (policies.mPasswordMode > 0) { if (policies.mPasswordMode > 0) {
// If the server requests a password policy of any kind (PIN, password, etc)
// and the user has a keyguard-disable package, then we cannot support the
// policy. In a rare case of a server that doesn't enforce PIN/Password, we
// won't do this test.
if (getKeyguardDisablePackage() != null) {
return false;
}
if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) { if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) {
return false; return false;
} }
@ -610,6 +627,55 @@ public class SecurityPolicy {
return dpm.isAdminActive(mAdminName); return dpm.isAdminActive(mAdminName);
} }
/**
* Invalidate the cached result of checking for lock screen disablers. Called by package
* add/replace/delete broadcasts.
*/
public synchronized void invalidateKeyguardCache() {
mKeyguardDisableChecked = false;
mKeyguardDisablePackageName = null;
}
/**
* Return the name of any app that is disabling the keyguard (lock screen) - in which case,
* we must treat the device as if required lock screen policies are not met.
*
* @return non-null if any app is disabling the keyguard
*/
public synchronized String getKeyguardDisablePackage() {
if (!mKeyguardDisableChecked) {
mKeyguardDisablePackageName = findKeyguardPermission();
mKeyguardDisableChecked = true;
}
return mKeyguardDisablePackageName;
}
/**
* Scan installed packages and look for DISABLE_KEYGUARD permission. Return the first
* installed packages (if any are found). This won't be cheap, so do it as little as possible.
*
* @return the display name of the package, or null if all clear
*/
private String findKeyguardPermission() {
PackageManager pm = mContext.getPackageManager();
List<PackageInfo> packages = pm.getInstalledPackages(0);
for (PackageInfo info : packages) {
// skip over system packages; they are allowed to have the permission
if (0 != (info.applicationInfo.flags &
(ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP))) {
continue;
}
// all other packages, check for specific permission
int permissionResult = pm.checkPermission(android.Manifest.permission.DISABLE_KEYGUARD,
info.packageName);
if (permissionResult == PackageManager.PERMISSION_GRANTED) {
Log.d(Email.LOG_TAG, "DISABLE_KEYGUARD found in " + info.packageName);
return info.applicationInfo.loadLabel(pm).toString();
}
}
return null;
}
/** /**
* Report admin component name - for making calls into device policy manager * Report admin component name - for making calls into device policy manager
*/ */

View File

@ -21,8 +21,11 @@ import com.android.email.SecurityPolicy;
import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Account;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManager;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
@ -40,8 +43,12 @@ public class AccountSecurity extends Activity {
private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity.setup.ACCOUNT_ID"; private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity.setup.ACCOUNT_ID";
private static final int DIALOG_KEYGUARD_DISABLED = 1;
private static final int REQUEST_ENABLE = 1; private static final int REQUEST_ENABLE = 1;
private String mKeyguardBlockerName;
/** /**
* Used for generating intent for this activity (which is intended to be launched * Used for generating intent for this activity (which is intended to be launched
* from a notification.) * from a notification.)
@ -59,6 +66,11 @@ public class AccountSecurity extends Activity {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
boolean finishNow = true;
// Return RESULT_OK in all cases unless explicitly changed (see below). This is ignored
// when we're launched from notification, but it's checked when we're launched from
// (and return to) AccountSetupNames.
setResult(Activity.RESULT_OK);
Intent i = getIntent(); Intent i = getIntent();
long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1); long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
@ -68,7 +80,8 @@ public class AccountSecurity extends Activity {
// TODO: spin up a thread to do this in the background, because of DB ops // TODO: spin up a thread to do this in the background, because of DB ops
Account account = Account.restoreAccountWithId(this, accountId); Account account = Account.restoreAccountWithId(this, accountId);
if (account != null) { if (account != null) {
if (account.mSecurityFlags != 0) { if ((account.mSecurityFlags != 0) ||
(0 != (account.mFlags & Account.FLAGS_SECURITY_HOLD))) {
// This account wants to control security // This account wants to control security
if (!security.isActiveAdmin()) { if (!security.isActiveAdmin()) {
// try to become active - must happen here in this activity, to get result // try to become active - must happen here in this activity, to get result
@ -83,12 +96,14 @@ public class AccountSecurity extends Activity {
return; return;
} else { } else {
// already active - try to set actual policies, finish, and return // already active - try to set actual policies, finish, and return
setActivePolicies(); finishNow = setActivePolicies();
} }
} }
} }
} }
finish(); if (finishNow) {
finish();
}
} }
/** /**
@ -96,11 +111,12 @@ public class AccountSecurity extends Activity {
*/ */
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
boolean finishNow = true;
switch (requestCode) { switch (requestCode) {
case REQUEST_ENABLE: case REQUEST_ENABLE:
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
// now active - try to set actual policies // now active - try to set actual policies
setActivePolicies(); finishNow = setActivePolicies();
} else { } else {
// failed - repost notification, and exit // failed - repost notification, and exit
final long accountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1); final long accountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1);
@ -115,31 +131,85 @@ public class AccountSecurity extends Activity {
} }
} }
} }
finish(); // This activity has no layout and in most cases serves only as a dispatcher for
// checking and adjusting device security policies. However, in a few cases, it
// presents an error dialog. In those cases, setActivePolicies() will return false, and
// we don't call finish() so the dialog is able to display.
if (finishNow) {
finish();
}
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
} }
/** /**
* Now that we are connected as an active device admin, try to set the device to the * Now that we are connected as an active device admin, try to set the device to the
* correct security level, and ask for a password if necessary. * correct security level, and ask for a password if necessary.
*
* @return true if OK to finish the activity (false if we posted a dialog)
*/ */
private void setActivePolicies() { private boolean setActivePolicies() {
SecurityPolicy sp = SecurityPolicy.getInstance(this); SecurityPolicy sp = SecurityPolicy.getInstance(this);
// check current security level - if sufficient, we're done! // check current security level - if sufficient, we're done!
if (sp.isActive(null)) { if (sp.isActive(null)) {
sp.clearAccountHoldFlags(); sp.clearAccountHoldFlags();
return; return true;
} }
// set current security level // set current security level
sp.setActivePolicies(); sp.setActivePolicies();
// check current security level - if sufficient, we're done! // check current security level - if sufficient, we're done!
if (sp.isActive(null)) { if (sp.isActive(null)) {
sp.clearAccountHoldFlags(); sp.clearAccountHoldFlags();
return; return true;
}
// if the problem is an app blocking the lock screen, notify the user
String keyguardBlocker = sp.getKeyguardDisablePackage();
if (keyguardBlocker != null) {
mKeyguardBlockerName = keyguardBlocker;
showDialog(DIALOG_KEYGUARD_DISABLED);
// Set activity to return RESULT_CANCELED (after dialog) to notify caller that we
// didn't configure policies completely.
setResult(Activity.RESULT_CANCELED);
return false;
} }
// if not sufficient, launch the activity to have the user set a new password. // if not sufficient, launch the activity to have the user set a new password.
Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
startActivity(intent); startActivity(intent);
// TODO: use startActivityForResult, and when we see the result, recheck to see
// if the user actually entered a sufficient password. If not, repost the notification.
return true;
} }
@Override
public Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_KEYGUARD_DISABLED:
return new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.keyguard_disabled_dlg_title)
.setMessage(getString(R.string.keyguard_disabled_dlg_message_fmt,
mKeyguardBlockerName))
.setNeutralButton(R.string.okay_action, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dismissDialog(DIALOG_KEYGUARD_DISABLED);
finish();
}
})
.setCancelable(false)
.create();
}
return super.onCreateDialog(id);
}
/**
* Update a cached dialog with current values (e.g. account name)
*/
@Override
public void onPrepareDialog(int id, Dialog dialog) {
switch (id) {
case DIALOG_KEYGUARD_DISABLED:
AlertDialog alert = (AlertDialog) dialog;
alert.setMessage(getString(R.string.keyguard_disabled_dlg_message_fmt,
mKeyguardBlockerName));
}
}
} }

View File

@ -19,6 +19,7 @@ package com.android.email.activity.setup;
import com.android.email.AccountBackupRestore; import com.android.email.AccountBackupRestore;
import com.android.email.R; import com.android.email.R;
import com.android.email.Utility; import com.android.email.Utility;
import com.android.email.activity.AccountFolderList;
import com.android.email.activity.Welcome; import com.android.email.activity.Welcome;
import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account; import com.android.email.provider.EmailContent.Account;
@ -146,12 +147,25 @@ public class AccountSetupNames extends Activity implements OnClickListener {
@Override @Override
public void onBackPressed() { public void onBackPressed() {
onBackPressed(true); // OK to "proceed" to next step (the account has been created)
}
private void onBackPressed(boolean okToProceed) {
boolean easFlowMode = getIntent().getBooleanExtra(EXTRA_EAS_FLOW, false); boolean easFlowMode = getIntent().getBooleanExtra(EXTRA_EAS_FLOW, false);
if (easFlowMode) { if (easFlowMode) {
AccountSetupBasics.actionAccountCreateFinishedEas(this); AccountSetupBasics.actionAccountCreateFinishedEas(this);
} else { } else {
if (mAccount != null) { if (mAccount != null) {
AccountSetupBasics.actionAccountCreateFinished(this, mAccount.mId); if (okToProceed) {
AccountSetupBasics.actionAccountCreateFinished(this, mAccount.mId);
} else {
// This is what we do if the account was created, but somebody called
// onBackPressed(false), indicating that the new account's inbox should not be
// entered. In this case, we'll just go back to the accounts list.
// We don't use the typical "Welcome" activity because if there is only one
// account, it will also try to visit the inbox of that account.
AccountFolderList.actionShowAccounts(this);
}
} else { } else {
// Safety check here; If mAccount is null (due to external issues or bugs) // Safety check here; If mAccount is null (due to external issues or bugs)
// just rewind back to Welcome, which can handle any configuration of accounts // just rewind back to Welcome, which can handle any configuration of accounts
@ -249,14 +263,15 @@ public class AccountSetupNames extends Activity implements OnClickListener {
/** /**
* Handle the eventual result from the security update activity * Handle the eventual result from the security update activity
*
* TODO: If the user doesn't update the security, don't go to the MessageList.
*/ */
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) { switch (requestCode) {
case REQUEST_SECURITY: case REQUEST_SECURITY:
onBackPressed(); // If we get RESULT_CANCEL, we tell onBackPressed that there was an error,
// and it won't try to take us to the MessageList of the new account.
onBackPressed(resultCode == Activity.RESULT_OK);
break;
} }
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
} }

View File

@ -18,6 +18,7 @@ package com.android.email.service;
import com.android.email.AccountBackupRestore; import com.android.email.AccountBackupRestore;
import com.android.email.Email; import com.android.email.Email;
import com.android.email.SecurityPolicy;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
@ -29,17 +30,24 @@ public class BootReceiver extends BroadcastReceiver {
// Restore accounts, if it has not happened already // Restore accounts, if it has not happened already
AccountBackupRestore.restoreAccountsIfNeeded(context); AccountBackupRestore.restoreAccountsIfNeeded(context);
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { String intentAction = intent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) {
// Returns true if there are any accounts // Returns true if there are any accounts
if (Email.setServicesEnabled(context)) { if (Email.setServicesEnabled(context)) {
MailService.actionReschedule(context); MailService.actionReschedule(context);
} }
} }
else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) { else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intentAction)) {
MailService.actionCancel(context); MailService.actionCancel(context);
} }
else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) { else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intentAction)) {
MailService.actionReschedule(context); MailService.actionReschedule(context);
} }
else if (Intent.ACTION_PACKAGE_ADDED.equals(intentAction) ||
Intent.ACTION_PACKAGE_REPLACED.equals(intentAction) ||
Intent.ACTION_PACKAGE_REMOVED.equals(intentAction)) {
SecurityPolicy.getInstance(context.getApplicationContext()).invalidateKeyguardCache();
}
} }
} }