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