SecurityPolicy.java revision 6d9d71683a067b7f2293b60f70dc60e151aa151b
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email; 18 19import com.android.email.service.EmailBroadcastProcessorService; 20import com.android.emailcommon.Logging; 21import com.android.emailcommon.provider.EmailContent; 22import com.android.emailcommon.provider.EmailContent.Account; 23import com.android.emailcommon.provider.EmailContent.AccountColumns; 24import com.android.emailcommon.provider.EmailContent.PolicyColumns; 25import com.android.emailcommon.provider.Policy; 26import com.android.emailcommon.utility.Utility; 27import com.google.common.annotations.VisibleForTesting; 28 29import android.app.admin.DeviceAdminInfo; 30import android.app.admin.DeviceAdminReceiver; 31import android.app.admin.DevicePolicyManager; 32import android.content.ComponentName; 33import android.content.ContentResolver; 34import android.content.ContentValues; 35import android.content.Context; 36import android.content.Intent; 37import android.database.Cursor; 38import android.os.Environment; 39import android.util.Log; 40 41/** 42 * Utility functions to support reading and writing security policies, and handshaking the device 43 * into and out of various security states. 44 */ 45public class SecurityPolicy { 46 private static final String TAG = "Email/SecurityPolicy"; 47 private static SecurityPolicy sInstance = null; 48 private Context mContext; 49 private DevicePolicyManager mDPM; 50 private ComponentName mAdminName; 51 private Policy mAggregatePolicy; 52 53 // Messages used for DevicePolicyManager callbacks 54 private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1; 55 private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2; 56 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3; 57 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4; 58 59 private static final String HAS_PASSWORD_EXPIRATION = 60 PolicyColumns.PASSWORD_EXPIRATION_DAYS + ">0"; 61 62 /** 63 * Get the security policy instance 64 */ 65 public synchronized static SecurityPolicy getInstance(Context context) { 66 if (sInstance == null) { 67 sInstance = new SecurityPolicy(context.getApplicationContext()); 68 // STOPSHIP: This is a workaround for b/4445007 69 // Make sure the DPM has our active policies 70 sInstance.setActivePolicies(); 71 } 72 return sInstance; 73 } 74 75 /** 76 * Private constructor (one time only) 77 */ 78 private SecurityPolicy(Context context) { 79 mContext = context.getApplicationContext(); 80 mDPM = null; 81 mAdminName = new ComponentName(context, PolicyAdmin.class); 82 mAggregatePolicy = null; 83 } 84 85 /** 86 * For testing only: Inject context into already-created instance 87 */ 88 /* package */ void setContext(Context context) { 89 mContext = context; 90 } 91 92 /** 93 * Compute the aggregate policy for all accounts that require it, and record it. 94 * 95 * The business logic is as follows: 96 * min password length take the max 97 * password mode take the max (strongest mode) 98 * max password fails take the min 99 * max screen lock time take the min 100 * require remote wipe take the max (logical or) 101 * password history take the max (strongest mode) 102 * password expiration take the min (strongest mode) 103 * password complex chars take the max (strongest mode) 104 * encryption take the max (logical or) 105 * encryption (external) take the max (logical or) 106 * 107 * @return a policy representing the strongest aggregate. If no policy sets are defined, 108 * a lightweight "nothing required" policy will be returned. Never null. 109 */ 110 /*package*/ Policy computeAggregatePolicy() { 111 boolean policiesFound = false; 112 Policy aggregate = new Policy(); 113 aggregate.mPasswordMinLength = Integer.MIN_VALUE; 114 aggregate.mPasswordMode = Integer.MIN_VALUE; 115 aggregate.mPasswordMaxFails = Integer.MAX_VALUE; 116 aggregate.mPasswordHistory = Integer.MIN_VALUE; 117 aggregate.mPasswordExpirationDays = Integer.MAX_VALUE; 118 aggregate.mPasswordComplexChars = Integer.MIN_VALUE; 119 aggregate.mMaxScreenLockTime = Integer.MAX_VALUE; 120 aggregate.mRequireRemoteWipe = false; 121 aggregate.mRequireEncryption = false; 122 aggregate.mRequireEncryptionExternal = false; 123 124 Cursor c = mContext.getContentResolver().query(Policy.CONTENT_URI, 125 Policy.CONTENT_PROJECTION, null, null, null); 126 Policy policy = new Policy(); 127 try { 128 while (c.moveToNext()) { 129 policy.restore(c); 130 if (Email.DEBUG) { 131 Log.d(TAG, "Aggregate from: " + policy); 132 } 133 aggregate.mPasswordMinLength = 134 Math.max(policy.mPasswordMinLength, aggregate.mPasswordMinLength); 135 aggregate.mPasswordMode = Math.max(policy.mPasswordMode, aggregate.mPasswordMode); 136 if (policy.mPasswordMaxFails > 0) { 137 aggregate.mPasswordMaxFails = 138 Math.min(policy.mPasswordMaxFails, aggregate.mPasswordMaxFails); 139 } 140 if (policy.mMaxScreenLockTime > 0) { 141 aggregate.mMaxScreenLockTime = Math.min(policy.mMaxScreenLockTime, 142 aggregate.mMaxScreenLockTime); 143 } 144 if (policy.mPasswordHistory > 0) { 145 aggregate.mPasswordHistory = 146 Math.max(policy.mPasswordHistory, aggregate.mPasswordHistory); 147 } 148 if (policy.mPasswordExpirationDays > 0) { 149 aggregate.mPasswordExpirationDays = 150 Math.min(policy.mPasswordExpirationDays, aggregate.mPasswordExpirationDays); 151 } 152 if (policy.mPasswordComplexChars > 0) { 153 aggregate.mPasswordComplexChars = Math.max(policy.mPasswordComplexChars, 154 aggregate.mPasswordComplexChars); 155 } 156 aggregate.mRequireRemoteWipe |= policy.mRequireRemoteWipe; 157 aggregate.mRequireEncryption |= policy.mRequireEncryption; 158 aggregate.mRequireEncryptionExternal |= policy.mRequireEncryptionExternal; 159 policiesFound = true; 160 } 161 } finally { 162 c.close(); 163 } 164 if (policiesFound) { 165 // final cleanup pass converts any untouched min/max values to zero (not specified) 166 if (aggregate.mPasswordMinLength == Integer.MIN_VALUE) aggregate.mPasswordMinLength = 0; 167 if (aggregate.mPasswordMode == Integer.MIN_VALUE) aggregate.mPasswordMode = 0; 168 if (aggregate.mPasswordMaxFails == Integer.MAX_VALUE) aggregate.mPasswordMaxFails = 0; 169 if (aggregate.mMaxScreenLockTime == Integer.MAX_VALUE) aggregate.mMaxScreenLockTime = 0; 170 if (aggregate.mPasswordHistory == Integer.MIN_VALUE) aggregate.mPasswordHistory = 0; 171 if (aggregate.mPasswordExpirationDays == Integer.MAX_VALUE) 172 aggregate.mPasswordExpirationDays = 0; 173 if (aggregate.mPasswordComplexChars == Integer.MIN_VALUE) 174 aggregate.mPasswordComplexChars = 0; 175 if (Email.DEBUG) { 176 Log.d(TAG, "Calculated Aggregate: " + aggregate); 177 } 178 return aggregate; 179 } 180 if (Email.DEBUG) { 181 Log.d(TAG, "Calculated Aggregate: no policy"); 182 } 183 return Policy.NO_POLICY; 184 } 185 186 /** 187 * Return updated aggregate policy, from cached value if possible 188 */ 189 public synchronized Policy getAggregatePolicy() { 190 if (mAggregatePolicy == null) { 191 mAggregatePolicy = computeAggregatePolicy(); 192 } 193 return mAggregatePolicy; 194 } 195 196 /** 197 * Get the dpm. This mainly allows us to make some utility calls without it, for testing. 198 */ 199 /* package */ synchronized DevicePolicyManager getDPM() { 200 if (mDPM == null) { 201 mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); 202 } 203 return mDPM; 204 } 205 206 /** 207 * API: Report that policies may have been updated due to rewriting values in an Account. 208 * @param accountId the account that has been updated, -1 if unknown/deleted 209 */ 210 public synchronized void policiesUpdated(long accountId) { 211 mAggregatePolicy = null; 212 } 213 214 /** 215 * API: Report that policies may have been updated *and* the caller vouches that the 216 * change is a reduction in policies. This forces an immediate change to device state. 217 * Typically used when deleting accounts, although we may use it for server-side policy 218 * rollbacks. 219 */ 220 public void reducePolicies() { 221 if (Email.DEBUG) { 222 Log.d(TAG, "reducePolicies"); 223 } 224 policiesUpdated(-1); 225 setActivePolicies(); 226 } 227 228 /** 229 * API: Query if the proposed set of policies are supported on the device. 230 * 231 * @param policies the polices that were requested 232 * @return boolean if supported 233 */ 234 public boolean isSupported(Policy policy) { 235 // IMPLEMENTATION: At this time, the only policy which might not be supported is 236 // encryption (which requires low-level systems support). Other policies are fully 237 // supported by the framework and do not need to be checked. 238 if (policy.mRequireEncryption) { 239 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 240 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { 241 return false; 242 } 243 } 244 if (policy.mRequireEncryptionExternal) { 245 // At this time, we only support "external encryption" when it is provided by virtue 246 // of emulating the external storage inside an encrypted device. 247 if (!policy.mRequireEncryption) return false; 248 if (Environment.isExternalStorageRemovable()) return false; 249 if (!Environment.isExternalStorageEmulated()) return false; 250 } 251 return true; 252 } 253 254 /** 255 * API: Remove any unsupported policies 256 * 257 * This is used when we have a set of polices that have been requested, but the server 258 * is willing to allow unsupported policies to be considered optional. 259 * 260 * @param policies the polices that were requested 261 * @return the same PolicySet if all are supported; A replacement PolicySet if any 262 * unsupported policies were removed 263 */ 264 public Policy clearUnsupportedPolicies(Policy policy) { 265 // IMPLEMENTATION: At this time, the only policy which might not be supported is 266 // encryption (which requires low-level systems support). Other policies are fully 267 // supported by the framework and do not need to be checked. 268 if (policy.mRequireEncryption) { 269 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 270 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { 271 policy.mRequireEncryption = false; 272 } 273 } 274 // At this time, we only support "external encryption" when it is provided by virtue 275 // of emulating the external storage inside an encrypted device. 276 if (policy.mRequireEncryptionExternal) { 277 if (Environment.isExternalStorageRemovable() 278 || !Environment.isExternalStorageEmulated()) { 279 policy.mRequireEncryptionExternal = false; 280 } 281 } 282 return policy; 283 } 284 285 /** 286 * API: Query used to determine if a given policy is "active" (the device is operating at 287 * the required security level). 288 * 289 * @param policies the policies requested, or null to check aggregate stored policies 290 * @return true if the requested policies are active, false if not. 291 */ 292 public boolean isActive(Policy policy) { 293 int reasons = getInactiveReasons(policy); 294 if (Email.DEBUG && (reasons != 0)) { 295 StringBuilder sb = new StringBuilder("isActive for " + policy + ": "); 296 if (reasons == 0) { 297 sb.append("true"); 298 } else { 299 sb.append("FALSE -> "); 300 } 301 if ((reasons & INACTIVE_NEED_ACTIVATION) != 0) { 302 sb.append("no_admin "); 303 } 304 if ((reasons & INACTIVE_NEED_CONFIGURATION) != 0) { 305 sb.append("config "); 306 } 307 if ((reasons & INACTIVE_NEED_PASSWORD) != 0) { 308 sb.append("password "); 309 } 310 if ((reasons & INACTIVE_NEED_ENCRYPTION) != 0) { 311 sb.append("encryption "); 312 } 313 Log.d(TAG, sb.toString()); 314 } 315 return reasons == 0; 316 } 317 318 /** 319 * Return bits from isActive: Device Policy Manager has not been activated 320 */ 321 public final static int INACTIVE_NEED_ACTIVATION = 1; 322 323 /** 324 * Return bits from isActive: Some required configuration is not correct (no user action). 325 */ 326 public final static int INACTIVE_NEED_CONFIGURATION = 2; 327 328 /** 329 * Return bits from isActive: Password needs to be set or updated 330 */ 331 public final static int INACTIVE_NEED_PASSWORD = 4; 332 333 /** 334 * Return bits from isActive: Encryption has not be enabled 335 */ 336 public final static int INACTIVE_NEED_ENCRYPTION = 8; 337 338 /** 339 * API: Query used to determine if a given policy is "active" (the device is operating at 340 * the required security level). 341 * 342 * This can be used when syncing a specific account, by passing a specific set of policies 343 * for that account. Or, it can be used at any time to compare the device 344 * state against the aggregate set of device policies stored in all accounts. 345 * 346 * This method is for queries only, and does not trigger any change in device state. 347 * 348 * NOTE: If there are multiple accounts with password expiration policies, the device 349 * password will be set to expire in the shortest required interval (most secure). This method 350 * will return 'false' as soon as the password expires - irrespective of which account caused 351 * the expiration. In other words, all accounts (that require expiration) will run/stop 352 * based on the requirements of the account with the shortest interval. 353 * 354 * @param policies the policies requested, or null to check aggregate stored policies 355 * @return zero if the requested policies are active, non-zero bits indicates that more work 356 * is needed (typically, by the user) before the required security polices are fully active. 357 */ 358 public int getInactiveReasons(Policy policy) { 359 // select aggregate set if needed 360 if (policy == null) { 361 policy = getAggregatePolicy(); 362 } 363 // quick check for the "empty set" of no policies 364 if (policy == Policy.NO_POLICY) { 365 return 0; 366 } 367 int reasons = 0; 368 DevicePolicyManager dpm = getDPM(); 369 if (isActiveAdmin()) { 370 // check each policy explicitly 371 if (policy.mPasswordMinLength > 0) { 372 if (dpm.getPasswordMinimumLength(mAdminName) < policy.mPasswordMinLength) { 373 reasons |= INACTIVE_NEED_PASSWORD; 374 } 375 } 376 if (policy.mPasswordMode > 0) { 377 if (dpm.getPasswordQuality(mAdminName) < policy.getDPManagerPasswordQuality()) { 378 reasons |= INACTIVE_NEED_PASSWORD; 379 } 380 if (!dpm.isActivePasswordSufficient()) { 381 reasons |= INACTIVE_NEED_PASSWORD; 382 } 383 } 384 if (policy.mMaxScreenLockTime > 0) { 385 // Note, we use seconds, dpm uses milliseconds 386 if (dpm.getMaximumTimeToLock(mAdminName) > policy.mMaxScreenLockTime * 1000) { 387 reasons |= INACTIVE_NEED_CONFIGURATION; 388 } 389 } 390 if (policy.mPasswordExpirationDays > 0) { 391 // confirm that expirations are currently set 392 long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName); 393 if (currentTimeout == 0 394 || currentTimeout > policy.getDPManagerPasswordExpirationTimeout()) { 395 reasons |= INACTIVE_NEED_PASSWORD; 396 } 397 // confirm that the current password hasn't expired 398 long expirationDate = dpm.getPasswordExpiration(mAdminName); 399 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 400 boolean expired = timeUntilExpiration < 0; 401 if (expired) { 402 reasons |= INACTIVE_NEED_PASSWORD; 403 } 404 } 405 if (policy.mPasswordHistory > 0) { 406 if (dpm.getPasswordHistoryLength(mAdminName) < policy.mPasswordHistory) { 407 reasons |= INACTIVE_NEED_PASSWORD; 408 } 409 } 410 if (policy.mPasswordComplexChars > 0) { 411 if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) { 412 reasons |= INACTIVE_NEED_PASSWORD; 413 } 414 } 415 if (policy.mRequireEncryption) { 416 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 417 if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { 418 reasons |= INACTIVE_NEED_ENCRYPTION; 419 } 420 } 421 // TODO: If we ever support external storage encryption as a first-class feature, 422 // it will need to be checked here. For now, if there is a policy request for 423 // external storage encryption, it's sufficient that we've activated internal 424 // storage encryption. 425 426 // password failures are counted locally - no test required here 427 // no check required for remote wipe (it's supported, if we're the admin) 428 429 // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances. 430 return reasons; 431 } 432 // return false, not active 433 return INACTIVE_NEED_ACTIVATION; 434 } 435 436 /** 437 * Set the requested security level based on the aggregate set of requests. 438 * If the set is empty, we release our device administration. If the set is non-empty, 439 * we only proceed if we are already active as an admin. 440 */ 441 public void setActivePolicies() { 442 DevicePolicyManager dpm = getDPM(); 443 // compute aggregate set of policies 444 Policy aggregatePolicy = getAggregatePolicy(); 445 // if empty set, detach from policy manager 446 if (aggregatePolicy == Policy.NO_POLICY) { 447 if (Email.DEBUG) { 448 Log.d(TAG, "setActivePolicies: none, remove admin"); 449 } 450 dpm.removeActiveAdmin(mAdminName); 451 } else if (isActiveAdmin()) { 452 if (Email.DEBUG) { 453 Log.d(TAG, "setActivePolicies: " + aggregatePolicy); 454 } 455 // set each policy in the policy manager 456 // password mode & length 457 dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality()); 458 dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength); 459 // screen lock time 460 dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000); 461 // local wipe (failed passwords limit) 462 dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails); 463 // password expiration (days until a password expires). API takes mSec. 464 dpm.setPasswordExpirationTimeout(mAdminName, 465 aggregatePolicy.getDPManagerPasswordExpirationTimeout()); 466 // password history length (number of previous passwords that may not be reused) 467 dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory); 468 // password minimum complex characters. 469 // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM, 470 // setting the quality to complex also defaults min symbols=1 and min numeric=1. 471 // We always / safely clear minSymbols & minNumeric to zero (there is no policy 472 // configuration in which we explicitly require a minimum number of digits or symbols.) 473 dpm.setPasswordMinimumSymbols(mAdminName, 0); 474 dpm.setPasswordMinimumNumeric(mAdminName, 0); 475 dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars); 476 // encryption required 477 dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption); 478 // TODO: If we ever support external storage encryption as a first-class feature, 479 // it will need to be set here. For now, if there is a policy request for 480 // external storage encryption, it's sufficient that we've activated internal 481 // storage encryption. 482 } 483 } 484 485 /** 486 * Convenience method; see javadoc below 487 */ 488 public static void setAccountHoldFlag(Context context, long accountId, boolean newState) { 489 Account account = Account.restoreAccountWithId(context, accountId); 490 if (account != null) { 491 setAccountHoldFlag(context, account, newState); 492 } 493 } 494 495 /** 496 * API: Set/Clear the "hold" flag in any account. This flag serves a dual purpose: 497 * Setting it gives us an indication that it was blocked, and clearing it gives EAS a 498 * signal to try syncing again. 499 * @param context 500 * @param account the account whose hold flag is to be set/cleared 501 * @param newState true = security hold, false = free to sync 502 */ 503 public static void setAccountHoldFlag(Context context, Account account, boolean newState) { 504 if (newState) { 505 account.mFlags |= Account.FLAGS_SECURITY_HOLD; 506 } else { 507 account.mFlags &= ~Account.FLAGS_SECURITY_HOLD; 508 } 509 ContentValues cv = new ContentValues(); 510 cv.put(AccountColumns.FLAGS, account.mFlags); 511 account.update(context, cv); 512 } 513 514 /** 515 * API: Sync service should call this any time a sync fails due to isActive() returning false. 516 * This will kick off the notify-acquire-admin-state process and/or increase the security level. 517 * The caller needs to write the required policies into this account before making this call. 518 * Should not be called from UI thread - uses DB lookups to prepare new notifications 519 * 520 * @param accountId the account for which sync cannot proceed 521 */ 522 public void policiesRequired(long accountId) { 523 Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId); 524 // In case the account has been deleted, just return 525 if (account == null) return; 526 if (Email.DEBUG) { 527 if (account.mPolicyKey == 0) { 528 Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": none"); 529 } else { 530 Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); 531 if (policy == null) { 532 Log.w(TAG, "No policy??"); 533 } else { 534 Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy); 535 } 536 } 537 } 538 539 // Mark the account as "on hold". 540 setAccountHoldFlag(mContext, account, true); 541 542 // Put up a notification 543 NotificationController.getInstance(mContext).showSecurityNeededNotification(account); 544 } 545 546 /** 547 * Called from the notification's intent receiver to register that the notification can be 548 * cleared now. 549 */ 550 public void clearNotification(long accountId) { 551 NotificationController.getInstance(mContext).cancelSecurityNeededNotification(); 552 } 553 554 /** 555 * API: Remote wipe (from server). This is final, there is no confirmation. It will only 556 * return to the caller if there is an unexpected failure. 557 */ 558 public void remoteWipe() { 559 DevicePolicyManager dpm = getDPM(); 560 if (dpm.isAdminActive(mAdminName)) { 561 dpm.wipeData(0); 562 } else { 563 Log.d(Logging.LOG_TAG, "Could not remote wipe because not device admin."); 564 } 565 } 566 /** 567 * If we are not the active device admin, try to become so. 568 * 569 * Also checks for any policies that we have added during the lifetime of this app. 570 * This catches the case where the user granted an earlier (smaller) set of policies 571 * but an app upgrade requires that new policies be granted. 572 * 573 * @return true if we are already active, false if we are not 574 */ 575 public boolean isActiveAdmin() { 576 DevicePolicyManager dpm = getDPM(); 577 return dpm.isAdminActive(mAdminName) 578 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD) 579 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE); 580 } 581 582 /** 583 * Report admin component name - for making calls into device policy manager 584 */ 585 public ComponentName getAdminComponent() { 586 return mAdminName; 587 } 588 589 /** 590 * Delete all accounts whose security flags aren't zero (i.e. they have security enabled). 591 * This method is synchronous, so it should normally be called within a worker thread (the 592 * exception being for unit tests) 593 * 594 * @param context the caller's context 595 */ 596 /*package*/ void deleteSecuredAccounts(Context context) { 597 ContentResolver cr = context.getContentResolver(); 598 // Find all accounts with security and delete them 599 Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, 600 Account.SECURITY_NONZERO_SELECTION, null, null); 601 try { 602 Log.w(TAG, "Email administration disabled; deleting " + c.getCount() + 603 " secured account(s)"); 604 while (c.moveToNext()) { 605 Controller.getInstance(context).deleteAccountSync( 606 c.getLong(EmailContent.ID_PROJECTION_COLUMN), context); 607 } 608 } finally { 609 c.close(); 610 } 611 policiesUpdated(-1); 612 } 613 614 /** 615 * Internal handler for enabled->disabled transitions. Deletes all secured accounts. 616 * Must call from worker thread, not on UI thread. 617 */ 618 /*package*/ void onAdminEnabled(boolean isEnabled) { 619 if (!isEnabled) { 620 deleteSecuredAccounts(mContext); 621 } 622 } 623 624 /** 625 * Handle password expiration - if any accounts appear to have triggered this, put up 626 * warnings, or even shut them down. 627 * 628 * NOTE: If there are multiple accounts with password expiration policies, the device 629 * password will be set to expire in the shortest required interval (most secure). The logic 630 * in this method operates based on the aggregate setting - irrespective of which account caused 631 * the expiration. In other words, all accounts (that require expiration) will run/stop 632 * based on the requirements of the account with the shortest interval. 633 */ 634 private void onPasswordExpiring(Context context) { 635 // 1. Do we have any accounts that matter here? 636 long nextExpiringAccountId = findShortestExpiration(context); 637 638 // 2. If not, exit immediately 639 if (nextExpiringAccountId == -1) { 640 return; 641 } 642 643 // 3. If yes, are we warning or expired? 644 long expirationDate = getDPM().getPasswordExpiration(mAdminName); 645 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 646 boolean expired = timeUntilExpiration < 0; 647 if (!expired) { 648 // 4. If warning, simply put up a generic notification and report that it came from 649 // the shortest-expiring account. 650 NotificationController.getInstance(mContext).showPasswordExpiringNotification( 651 nextExpiringAccountId); 652 } else { 653 // 5. Actually expired - find all accounts that expire passwords, and wipe them 654 boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context)); 655 if (wiped) { 656 NotificationController.getInstance(mContext).showPasswordExpiredNotification( 657 nextExpiringAccountId); 658 } 659 } 660 } 661 662 /** 663 * Find the account with the shortest expiration time. This is always assumed to be 664 * the account that forces the password to be refreshed. 665 * @return -1 if no expirations, or accountId if one is found 666 */ 667 @VisibleForTesting 668 /*package*/ static long findShortestExpiration(Context context) { 669 long policyId = Utility.getFirstRowLong(context, Policy.CONTENT_URI, Policy.ID_PROJECTION, 670 HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC", 671 EmailContent.ID_PROJECTION_COLUMN, -1L); 672 if (policyId < 0) return -1L; 673 return Policy.getAccountIdWithPolicyKey(context, policyId); 674 } 675 676 /** 677 * For all accounts that require password expiration, put them in security hold and wipe 678 * their data. 679 * @param context 680 * @param controller 681 * @return true if one or more accounts were wiped 682 */ 683 @VisibleForTesting 684 /*package*/ static boolean wipeExpiredAccounts(Context context, Controller controller) { 685 boolean result = false; 686 Cursor c = context.getContentResolver().query(Policy.CONTENT_URI, 687 Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null); 688 try { 689 while (c.moveToNext()) { 690 long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN); 691 long accountId = Policy.getAccountIdWithPolicyKey(context, policyId); 692 if (accountId < 0) continue; 693 Account account = Account.restoreAccountWithId(context, accountId); 694 if (account != null) { 695 // Mark the account as "on hold". 696 setAccountHoldFlag(context, account, true); 697 // Erase data 698 controller.deleteSyncedDataSync(accountId); 699 // Report one or more were found 700 result = true; 701 } 702 } 703 } finally { 704 c.close(); 705 } 706 return result; 707 } 708 709 /** 710 * Callback from EmailBroadcastProcessorService. This provides the workers for the 711 * DeviceAdminReceiver calls. These should perform the work directly and not use async 712 * threads for completion. 713 */ 714 public static void onDeviceAdminReceiverMessage(Context context, int message) { 715 SecurityPolicy instance = SecurityPolicy.getInstance(context); 716 switch (message) { 717 case DEVICE_ADMIN_MESSAGE_ENABLED: 718 instance.onAdminEnabled(true); 719 break; 720 case DEVICE_ADMIN_MESSAGE_DISABLED: 721 instance.onAdminEnabled(false); 722 break; 723 case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED: 724 // TODO make a small helper for this 725 // Clear security holds (if any) 726 Account.clearSecurityHoldOnAllAccounts(context); 727 // Cancel any active notifications (if any are posted) 728 NotificationController.getInstance(context).cancelPasswordExpirationNotifications(); 729 break; 730 case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING: 731 instance.onPasswordExpiring(instance.mContext); 732 break; 733 } 734 } 735 736 /** 737 * Device Policy administrator. This is primarily a listener for device state changes. 738 * Note: This is instantiated by incoming messages. 739 * Note: This is actually a BroadcastReceiver and must remain within the guidelines required 740 * for proper behavior, including avoidance of ANRs. 741 * Note: We do not implement onPasswordFailed() because the default behavior of the 742 * DevicePolicyManager - complete local wipe after 'n' failures - is sufficient. 743 */ 744 public static class PolicyAdmin extends DeviceAdminReceiver { 745 746 /** 747 * Called after the administrator is first enabled. 748 */ 749 @Override 750 public void onEnabled(Context context, Intent intent) { 751 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 752 DEVICE_ADMIN_MESSAGE_ENABLED); 753 } 754 755 /** 756 * Called prior to the administrator being disabled. 757 */ 758 @Override 759 public void onDisabled(Context context, Intent intent) { 760 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 761 DEVICE_ADMIN_MESSAGE_DISABLED); 762 } 763 764 /** 765 * Called when the user asks to disable administration; we return a warning string that 766 * will be presented to the user 767 */ 768 @Override 769 public CharSequence onDisableRequested(Context context, Intent intent) { 770 return context.getString(R.string.disable_admin_warning); 771 } 772 773 /** 774 * Called after the user has changed their password. 775 */ 776 @Override 777 public void onPasswordChanged(Context context, Intent intent) { 778 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 779 DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED); 780 } 781 782 /** 783 * Called when device password is expiring 784 */ 785 @Override 786 public void onPasswordExpiring(Context context, Intent intent) { 787 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 788 DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING); 789 } 790 } 791} 792