SecurityPolicy.java revision 22759bacd95385d95d3d9321f490763df1aba89d
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 int reasons = getInactiveReasons(policies); 295 return reasons == 0; 296 } 297 298 /** 299 * Return bits from isActive: Device Policy Manager has not been activated 300 */ 301 public final static int INACTIVE_NEED_ACTIVATION = 1; 302 303 /** 304 * Return bits from isActive: Some required configuration is not correct (no user action). 305 */ 306 public final static int INACTIVE_NEED_CONFIGURATION = 2; 307 308 /** 309 * Return bits from isActive: Password needs to be set or updated 310 */ 311 public final static int INACTIVE_NEED_PASSWORD = 4; 312 313 /** 314 * Return bits from isActive: Encryption has not be enabled 315 */ 316 public final static int INACTIVE_NEED_ENCRYPTION = 8; 317 318 /** 319 * API: Query used to determine if a given policy is "active" (the device is operating at 320 * the required security level). 321 * 322 * This can be used when syncing a specific account, by passing a specific set of policies 323 * for that account. Or, it can be used at any time to compare the device 324 * state against the aggregate set of device policies stored in all accounts. 325 * 326 * This method is for queries only, and does not trigger any change in device state. 327 * 328 * NOTE: If there are multiple accounts with password expiration policies, the device 329 * password will be set to expire in the shortest required interval (most secure). This method 330 * will return 'false' as soon as the password expires - irrespective of which account caused 331 * the expiration. In other words, all accounts (that require expiration) will run/stop 332 * based on the requirements of the account with the shortest interval. 333 * 334 * @param policies the policies requested, or null to check aggregate stored policies 335 * @return zero if the requested policies are active, non-zero bits indicates that more work 336 * is needed (typically, by the user) before the required security polices are fully active. 337 */ 338 public int getInactiveReasons(PolicySet policies) { 339 // select aggregate set if needed 340 if (policies == null) { 341 policies = getAggregatePolicy(); 342 } 343 // quick check for the "empty set" of no policies 344 if (policies == NO_POLICY_SET) { 345 return 0; 346 } 347 int reasons = 0; 348 DevicePolicyManager dpm = getDPM(); 349 if (isActiveAdmin()) { 350 // check each policy explicitly 351 if (policies.mMinPasswordLength > 0) { 352 if (dpm.getPasswordMinimumLength(mAdminName) < policies.mMinPasswordLength) { 353 reasons |= INACTIVE_NEED_PASSWORD; 354 } 355 } 356 if (policies.mPasswordMode > 0) { 357 if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) { 358 reasons |= INACTIVE_NEED_PASSWORD; 359 } 360 if (!dpm.isActivePasswordSufficient()) { 361 reasons |= INACTIVE_NEED_PASSWORD; 362 } 363 } 364 if (policies.mMaxScreenLockTime > 0) { 365 // Note, we use seconds, dpm uses milliseconds 366 if (dpm.getMaximumTimeToLock(mAdminName) > policies.mMaxScreenLockTime * 1000) { 367 reasons |= INACTIVE_NEED_CONFIGURATION; 368 } 369 } 370 if (policies.mPasswordExpirationDays > 0) { 371 // confirm that expirations are currently set 372 long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName); 373 if (currentTimeout == 0 374 || currentTimeout > policies.getDPManagerPasswordExpirationTimeout()) { 375 reasons |= INACTIVE_NEED_PASSWORD; 376 } 377 // confirm that the current password hasn't expired 378 long expirationDate = dpm.getPasswordExpiration(mAdminName); 379 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 380 boolean expired = timeUntilExpiration < 0; 381 if (expired) { 382 reasons |= INACTIVE_NEED_PASSWORD; 383 } 384 } 385 if (policies.mPasswordHistory > 0) { 386 if (dpm.getPasswordHistoryLength(mAdminName) < policies.mPasswordHistory) { 387 reasons |= INACTIVE_NEED_PASSWORD; 388 } 389 } 390 if (policies.mPasswordComplexChars > 0) { 391 if (dpm.getPasswordMinimumNonLetter(mAdminName) < policies.mPasswordComplexChars) { 392 reasons |= INACTIVE_NEED_PASSWORD; 393 } 394 } 395 if (policies.mRequireEncryption) { 396 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 397 if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { 398 reasons |= INACTIVE_NEED_ENCRYPTION; 399 } 400 } 401 // TODO: If we ever support external storage encryption as a first-class feature, 402 // it will need to be checked here. For now, if there is a policy request for 403 // external storage encryption, it's sufficient that we've activated internal 404 // storage encryption. 405 406 // password failures are counted locally - no test required here 407 // no check required for remote wipe (it's supported, if we're the admin) 408 409 // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances. 410 return reasons; 411 } 412 // return false, not active 413 return INACTIVE_NEED_ACTIVATION; 414 } 415 416 /** 417 * Set the requested security level based on the aggregate set of requests. 418 * If the set is empty, we release our device administration. If the set is non-empty, 419 * we only proceed if we are already active as an admin. 420 */ 421 public void setActivePolicies() { 422 DevicePolicyManager dpm = getDPM(); 423 // compute aggregate set of policies 424 PolicySet policies = getAggregatePolicy(); 425 // if empty set, detach from policy manager 426 if (policies == NO_POLICY_SET) { 427 dpm.removeActiveAdmin(mAdminName); 428 } else if (isActiveAdmin()) { 429 // set each policy in the policy manager 430 // password mode & length 431 dpm.setPasswordQuality(mAdminName, policies.getDPManagerPasswordQuality()); 432 dpm.setPasswordMinimumLength(mAdminName, policies.mMinPasswordLength); 433 // screen lock time 434 dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000); 435 // local wipe (failed passwords limit) 436 dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails); 437 // password expiration (days until a password expires). API takes mSec. 438 dpm.setPasswordExpirationTimeout(mAdminName, 439 policies.getDPManagerPasswordExpirationTimeout()); 440 // password history length (number of previous passwords that may not be reused) 441 dpm.setPasswordHistoryLength(mAdminName, policies.mPasswordHistory); 442 // password minimum complex characters. 443 // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM, 444 // setting the quality to complex also defaults min symbols=1 and min numeric=1. 445 // We always / safely clear minSymbols & minNumeric to zero (there is no policy 446 // configuration in which we explicitly require a minimum number of digits or symbols.) 447 dpm.setPasswordMinimumSymbols(mAdminName, 0); 448 dpm.setPasswordMinimumNumeric(mAdminName, 0); 449 dpm.setPasswordMinimumNonLetter(mAdminName, policies.mPasswordComplexChars); 450 // encryption required 451 dpm.setStorageEncryption(mAdminName, policies.mRequireEncryption); 452 // TODO: If we ever support external storage encryption as a first-class feature, 453 // it will need to be set here. For now, if there is a policy request for 454 // external storage encryption, it's sufficient that we've activated internal 455 // storage encryption. 456 } 457 } 458 459 /** 460 * Convenience method; see javadoc below 461 */ 462 public static void setAccountHoldFlag(Context context, long accountId, boolean newState) { 463 Account account = Account.restoreAccountWithId(context, accountId); 464 if (account != null) { 465 setAccountHoldFlag(context, account, newState); 466 } 467 } 468 469 /** 470 * API: Set/Clear the "hold" flag in any account. This flag serves a dual purpose: 471 * Setting it gives us an indication that it was blocked, and clearing it gives EAS a 472 * signal to try syncing again. 473 * @param context 474 * @param account the account whose hold flag is to be set/cleared 475 * @param newState true = security hold, false = free to sync 476 */ 477 public static void setAccountHoldFlag(Context context, Account account, boolean newState) { 478 if (newState) { 479 account.mFlags |= Account.FLAGS_SECURITY_HOLD; 480 } else { 481 account.mFlags &= ~Account.FLAGS_SECURITY_HOLD; 482 } 483 ContentValues cv = new ContentValues(); 484 cv.put(AccountColumns.FLAGS, account.mFlags); 485 account.update(context, cv); 486 } 487 488 /** 489 * API: Sync service should call this any time a sync fails due to isActive() returning false. 490 * This will kick off the notify-acquire-admin-state process and/or increase the security level. 491 * The caller needs to write the required policies into this account before making this call. 492 * Should not be called from UI thread - uses DB lookups to prepare new notifications 493 * 494 * @param accountId the account for which sync cannot proceed 495 */ 496 public void policiesRequired(long accountId) { 497 Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId); 498 // In case the account has been deleted, just return 499 if (account == null) return; 500 501 // Mark the account as "on hold". 502 setAccountHoldFlag(mContext, account, true); 503 504 // Put up a notification 505 String tickerText = mContext.getString(R.string.security_notification_ticker_fmt, 506 account.getDisplayName()); 507 String contentTitle = mContext.getString(R.string.security_notification_content_title); 508 String contentText = account.getDisplayName(); 509 Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, accountId, true); 510 NotificationController.getInstance(mContext).postAccountNotification( 511 account, tickerText, contentTitle, contentText, intent, 512 NotificationController.NOTIFICATION_ID_SECURITY_NEEDED); 513 } 514 515 /** 516 * Called from the notification's intent receiver to register that the notification can be 517 * cleared now. 518 */ 519 public void clearNotification(long accountId) { 520 NotificationController.getInstance(mContext).cancelNotification( 521 NotificationController.NOTIFICATION_ID_SECURITY_NEEDED); 522 } 523 524 /** 525 * API: Remote wipe (from server). This is final, there is no confirmation. It will only 526 * return to the caller if there is an unexpected failure. 527 */ 528 public void remoteWipe() { 529 DevicePolicyManager dpm = getDPM(); 530 if (dpm.isAdminActive(mAdminName)) { 531 dpm.wipeData(0); 532 } else { 533 Log.d(Logging.LOG_TAG, "Could not remote wipe because not device admin."); 534 } 535 } 536 /** 537 * If we are not the active device admin, try to become so. 538 * 539 * Also checks for any policies that we have added during the lifetime of this app. 540 * This catches the case where the user granted an earlier (smaller) set of policies 541 * but an app upgrade requires that new policies be granted. 542 * 543 * @return true if we are already active, false if we are not 544 */ 545 public boolean isActiveAdmin() { 546 DevicePolicyManager dpm = getDPM(); 547 return dpm.isAdminActive(mAdminName) 548 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD) 549 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE); 550 } 551 552 /** 553 * Report admin component name - for making calls into device policy manager 554 */ 555 public ComponentName getAdminComponent() { 556 return mAdminName; 557 } 558 559 /** 560 * Delete all accounts whose security flags aren't zero (i.e. they have security enabled). 561 * This method is synchronous, so it should normally be called within a worker thread (the 562 * exception being for unit tests) 563 * 564 * @param context the caller's context 565 */ 566 /*package*/ void deleteSecuredAccounts(Context context) { 567 ContentResolver cr = context.getContentResolver(); 568 // Find all accounts with security and delete them 569 Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, 570 AccountColumns.SECURITY_FLAGS + "!=0", null, null); 571 try { 572 Log.w(TAG, "Email administration disabled; deleting " + c.getCount() + 573 " secured account(s)"); 574 while (c.moveToNext()) { 575 Controller.getInstance(context).deleteAccountSync( 576 c.getLong(EmailContent.ID_PROJECTION_COLUMN), context); 577 } 578 } finally { 579 c.close(); 580 } 581 updatePolicies(-1); 582 } 583 584 /** 585 * Internal handler for enabled->disabled transitions. Deletes all secured accounts. 586 * Must call from worker thread, not on UI thread. 587 */ 588 /*package*/ void onAdminEnabled(boolean isEnabled) { 589 if (!isEnabled) { 590 deleteSecuredAccounts(mContext); 591 } 592 } 593 594 /** 595 * Handle password expiration - if any accounts appear to have triggered this, put up 596 * warnings, or even shut them down. 597 * 598 * NOTE: If there are multiple accounts with password expiration policies, the device 599 * password will be set to expire in the shortest required interval (most secure). The logic 600 * in this method operates based on the aggregate setting - irrespective of which account caused 601 * the expiration. In other words, all accounts (that require expiration) will run/stop 602 * based on the requirements of the account with the shortest interval. 603 */ 604 private void onPasswordExpiring(Context context) { 605 // 1. Do we have any accounts that matter here? 606 long nextExpiringAccountId = findShortestExpiration(context); 607 608 // 2. If not, exit immediately 609 if (nextExpiringAccountId == -1) { 610 return; 611 } 612 613 // 3. If yes, are we warning or expired? 614 long expirationDate = getDPM().getPasswordExpiration(mAdminName); 615 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 616 boolean expired = timeUntilExpiration < 0; 617 if (!expired) { 618 // 4. If warning, simply put up a generic notification and report that it came from 619 // the shortest-expiring account. 620 Account account = Account.restoreAccountWithId(context, nextExpiringAccountId); 621 if (account == null) return; 622 Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(context, 623 nextExpiringAccountId, false); 624 String ticker = context.getString( 625 R.string.password_expire_warning_ticker_fmt, account.getDisplayName()); 626 String contentTitle = context.getString( 627 R.string.password_expire_warning_content_title); 628 String contentText = account.getDisplayName(); 629 NotificationController nc = NotificationController.getInstance(mContext); 630 nc.postAccountNotification(account, ticker, contentTitle, contentText, intent, 631 NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING); 632 } else { 633 // 5. Actually expired - find all accounts that expire passwords, and wipe them 634 boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context)); 635 if (wiped) { 636 // Post notification 637 Account account = Account.restoreAccountWithId(context, nextExpiringAccountId); 638 if (account == null) return; 639 Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(context, 640 nextExpiringAccountId, true); 641 String ticker = context.getString(R.string.password_expired_ticker); 642 String contentTitle = context.getString(R.string.password_expired_content_title); 643 String contentText = account.getDisplayName(); 644 NotificationController nc = NotificationController.getInstance(mContext); 645 nc.postAccountNotification(account, ticker, contentTitle, 646 contentText, intent, 647 NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED); 648 } 649 } 650 } 651 652 /** 653 * Find the account with the shortest expiration time. This is always assumed to be 654 * the account that forces the password to be refreshed. 655 * @return -1 if no expirations, or accountId if one is found 656 */ 657 /* package */ static long findShortestExpiration(Context context) { 658 long nextExpiringAccountId = -1; 659 long shortestExpiration = Long.MAX_VALUE; 660 Cursor c = context.getContentResolver().query(Account.CONTENT_URI, 661 ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); 662 try { 663 while (c.moveToNext()) { 664 long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); 665 if (flags != 0) { 666 PolicySet p = new PolicySet(flags); 667 if (p.mPasswordExpirationDays > 0 && 668 p.mPasswordExpirationDays < shortestExpiration) { 669 nextExpiringAccountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID); 670 shortestExpiration = p.mPasswordExpirationDays; 671 } 672 } 673 } 674 } finally { 675 c.close(); 676 } 677 return nextExpiringAccountId; 678 } 679 680 /** 681 * For all accounts that require password expiration, put them in security hold and wipe 682 * their data. 683 * @param context 684 * @param controller 685 * @return true if one or more accounts were wiped 686 */ 687 /* package */ static boolean wipeExpiredAccounts(Context context, Controller controller) { 688 boolean result = false; 689 Cursor c = context.getContentResolver().query(Account.CONTENT_URI, 690 ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); 691 try { 692 while (c.moveToNext()) { 693 long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); 694 if (flags != 0) { 695 PolicySet p = new PolicySet(flags); 696 if (p.mPasswordExpirationDays > 0) { 697 long accountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID); 698 Account account = Account.restoreAccountWithId(context, accountId); 699 if (account != null) { 700 // Mark the account as "on hold". 701 setAccountHoldFlag(context, account, true); 702 // Erase data 703 controller.deleteSyncedDataSync(accountId); 704 // Report one or more were found 705 result = true; 706 } 707 } 708 } 709 } 710 } finally { 711 c.close(); 712 } 713 return result; 714 } 715 716 /** 717 * Callback from EmailBroadcastProcessorService. This provides the workers for the 718 * DeviceAdminReceiver calls. These should perform the work directly and not use async 719 * threads for completion. 720 */ 721 public static void onDeviceAdminReceiverMessage(Context context, int message) { 722 SecurityPolicy instance = SecurityPolicy.getInstance(context); 723 switch (message) { 724 case DEVICE_ADMIN_MESSAGE_ENABLED: 725 instance.onAdminEnabled(true); 726 break; 727 case DEVICE_ADMIN_MESSAGE_DISABLED: 728 instance.onAdminEnabled(false); 729 break; 730 case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED: 731 // TODO make a small helper for this 732 // Clear security holds (if any) 733 Account.clearSecurityHoldOnAllAccounts(context); 734 // Cancel any active notifications (if any are posted) 735 NotificationController nc = NotificationController.getInstance(context); 736 nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING); 737 nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED); 738 break; 739 case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING: 740 instance.onPasswordExpiring(instance.mContext); 741 break; 742 } 743 } 744 745 /** 746 * Device Policy administrator. This is primarily a listener for device state changes. 747 * Note: This is instantiated by incoming messages. 748 * Note: This is actually a BroadcastReceiver and must remain within the guidelines required 749 * for proper behavior, including avoidance of ANRs. 750 * Note: We do not implement onPasswordFailed() because the default behavior of the 751 * DevicePolicyManager - complete local wipe after 'n' failures - is sufficient. 752 */ 753 public static class PolicyAdmin extends DeviceAdminReceiver { 754 755 /** 756 * Called after the administrator is first enabled. 757 */ 758 @Override 759 public void onEnabled(Context context, Intent intent) { 760 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 761 DEVICE_ADMIN_MESSAGE_ENABLED); 762 } 763 764 /** 765 * Called prior to the administrator being disabled. 766 */ 767 @Override 768 public void onDisabled(Context context, Intent intent) { 769 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 770 DEVICE_ADMIN_MESSAGE_DISABLED); 771 } 772 773 /** 774 * Called when the user asks to disable administration; we return a warning string that 775 * will be presented to the user 776 */ 777 @Override 778 public CharSequence onDisableRequested(Context context, Intent intent) { 779 return context.getString(R.string.disable_admin_warning); 780 } 781 782 /** 783 * Called after the user has changed their password. 784 */ 785 @Override 786 public void onPasswordChanged(Context context, Intent intent) { 787 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 788 DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED); 789 } 790 791 /** 792 * Called when device password is expiring 793 */ 794 @Override 795 public void onPasswordExpiring(Context context, Intent intent) { 796 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 797 DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING); 798 } 799 } 800} 801