SecurityPolicy.java revision df982bdb471c4465885d05d177d4f8f183ae096f
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 android.app.admin.DeviceAdminInfo; 20import android.app.admin.DeviceAdminReceiver; 21import android.app.admin.DevicePolicyManager; 22import android.content.ComponentName; 23import android.content.ContentProviderOperation; 24import android.content.ContentResolver; 25import android.content.ContentUris; 26import android.content.ContentValues; 27import android.content.Context; 28import android.content.Intent; 29import android.content.OperationApplicationException; 30import android.database.Cursor; 31import android.os.Build; 32import android.os.RemoteException; 33import android.util.Log; 34 35import com.android.email.service.EmailBroadcastProcessorService; 36import com.android.emailcommon.Logging; 37import com.android.emailcommon.provider.Account; 38import com.android.emailcommon.provider.EmailContent; 39import com.android.emailcommon.provider.EmailContent.AccountColumns; 40import com.android.emailcommon.provider.EmailContent.PolicyColumns; 41import com.android.emailcommon.provider.Policy; 42import com.android.emailcommon.utility.TextUtilities; 43import com.android.emailcommon.utility.Utility; 44import com.google.common.annotations.VisibleForTesting; 45 46import java.util.ArrayList; 47 48/** 49 * Utility functions to support reading and writing security policies, and handshaking the device 50 * into and out of various security states. 51 */ 52public class SecurityPolicy { 53 private static final String TAG = "Email/SecurityPolicy"; 54 private static SecurityPolicy sInstance = null; 55 private Context mContext; 56 private DevicePolicyManager mDPM; 57 private final ComponentName mAdminName; 58 private Policy mAggregatePolicy; 59 60 // Messages used for DevicePolicyManager callbacks 61 private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1; 62 private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2; 63 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3; 64 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4; 65 66 private static final String HAS_PASSWORD_EXPIRATION = 67 PolicyColumns.PASSWORD_EXPIRATION_DAYS + ">0"; 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 * 110 * @return a policy representing the strongest aggregate. If no policy sets are defined, 111 * a lightweight "nothing required" policy will be returned. Never null. 112 */ 113 @VisibleForTesting 114 Policy computeAggregatePolicy() { 115 boolean policiesFound = false; 116 Policy ap = new Policy(); 117 ap.mPasswordMinLength = Integer.MIN_VALUE; 118 ap.mPasswordMode = Integer.MIN_VALUE; 119 ap.mPasswordMaxFails = Integer.MAX_VALUE; 120 ap.mPasswordHistory = Integer.MIN_VALUE; 121 ap.mPasswordExpirationDays = Integer.MAX_VALUE; 122 ap.mPasswordComplexChars = Integer.MIN_VALUE; 123 ap.mMaxScreenLockTime = Integer.MAX_VALUE; 124 ap.mRequireRemoteWipe = false; 125 ap.mRequireEncryption = false; 126 127 // This can never be supported at this time. It exists only for historic reasons where 128 // this was able to be supported prior to the introduction of proper removable storage 129 // support for external storage. 130 ap.mRequireEncryptionExternal = false; 131 132 Cursor c = mContext.getContentResolver().query(Policy.CONTENT_URI, 133 Policy.CONTENT_PROJECTION, null, null, null); 134 Policy policy = new Policy(); 135 try { 136 while (c.moveToNext()) { 137 policy.restore(c); 138 if (Email.DEBUG) { 139 Log.d(TAG, "Aggregate from: " + policy); 140 } 141 ap.mPasswordMinLength = Math.max(policy.mPasswordMinLength, ap.mPasswordMinLength); 142 ap.mPasswordMode = Math.max(policy.mPasswordMode, ap.mPasswordMode); 143 if (policy.mPasswordMaxFails > 0) { 144 ap.mPasswordMaxFails = 145 Math.min(policy.mPasswordMaxFails, ap.mPasswordMaxFails); 146 } 147 if (policy.mMaxScreenLockTime > 0) { 148 ap.mMaxScreenLockTime = 149 Math.min(policy.mMaxScreenLockTime, ap.mMaxScreenLockTime); 150 } 151 if (policy.mPasswordHistory > 0) { 152 ap.mPasswordHistory = 153 Math.max(policy.mPasswordHistory, ap.mPasswordHistory); 154 } 155 if (policy.mPasswordExpirationDays > 0) { 156 ap.mPasswordExpirationDays = 157 Math.min(policy.mPasswordExpirationDays, ap.mPasswordExpirationDays); 158 } 159 if (policy.mPasswordComplexChars > 0) { 160 ap.mPasswordComplexChars = 161 Math.max(policy.mPasswordComplexChars, ap.mPasswordComplexChars); 162 } 163 ap.mRequireRemoteWipe |= policy.mRequireRemoteWipe; 164 ap.mRequireEncryption |= policy.mRequireEncryption; 165 ap.mDontAllowCamera |= policy.mDontAllowCamera; 166 policiesFound = true; 167 } 168 } finally { 169 c.close(); 170 } 171 if (policiesFound) { 172 // final cleanup pass converts any untouched min/max values to zero (not specified) 173 if (ap.mPasswordMinLength == Integer.MIN_VALUE) ap.mPasswordMinLength = 0; 174 if (ap.mPasswordMode == Integer.MIN_VALUE) ap.mPasswordMode = 0; 175 if (ap.mPasswordMaxFails == Integer.MAX_VALUE) ap.mPasswordMaxFails = 0; 176 if (ap.mMaxScreenLockTime == Integer.MAX_VALUE) ap.mMaxScreenLockTime = 0; 177 if (ap.mPasswordHistory == Integer.MIN_VALUE) ap.mPasswordHistory = 0; 178 if (ap.mPasswordExpirationDays == Integer.MAX_VALUE) 179 ap.mPasswordExpirationDays = 0; 180 if (ap.mPasswordComplexChars == Integer.MIN_VALUE) 181 ap.mPasswordComplexChars = 0; 182 if (Email.DEBUG) { 183 Log.d(TAG, "Calculated Aggregate: " + ap); 184 } 185 return ap; 186 } 187 if (Email.DEBUG) { 188 Log.d(TAG, "Calculated Aggregate: no policy"); 189 } 190 return Policy.NO_POLICY; 191 } 192 193 /** 194 * Return updated aggregate policy, from cached value if possible 195 */ 196 public synchronized Policy getAggregatePolicy() { 197 if (mAggregatePolicy == null) { 198 mAggregatePolicy = computeAggregatePolicy(); 199 } 200 return mAggregatePolicy; 201 } 202 203 /** 204 * Get the dpm. This mainly allows us to make some utility calls without it, for testing. 205 */ 206 /* package */ synchronized DevicePolicyManager getDPM() { 207 if (mDPM == null) { 208 mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); 209 } 210 return mDPM; 211 } 212 213 /** 214 * API: Report that policies may have been updated due to rewriting values in an Account. 215 * @param accountId the account that has been updated, -1 if unknown/deleted 216 */ 217 public synchronized void policiesUpdated(long accountId) { 218 mAggregatePolicy = null; 219 } 220 221 /** 222 * API: Report that policies may have been updated *and* the caller vouches that the 223 * change is a reduction in policies. This forces an immediate change to device state. 224 * Typically used when deleting accounts, although we may use it for server-side policy 225 * rollbacks. 226 */ 227 public void reducePolicies() { 228 if (Email.DEBUG) { 229 Log.d(TAG, "reducePolicies"); 230 } 231 policiesUpdated(-1); 232 setActivePolicies(); 233 } 234 235 /** 236 * API: Query if the proposed set of policies are supported on the device. 237 * 238 * @param policy the polices that were requested 239 * @return boolean if supported 240 */ 241 public boolean isSupported(Policy policy) { 242 // IMPLEMENTATION: At this time, the only policy which might not be supported is 243 // encryption (which requires low-level systems support). Other policies are fully 244 // supported by the framework and do not need to be checked. 245 if (policy.mRequireEncryption) { 246 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 247 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { 248 return false; 249 } 250 } 251 252 // If we ever support devices that can't disable cameras for any reason, we should 253 // indicate as such in the mDontAllowCamera policy 254 255 return true; 256 } 257 258 /** 259 * API: Remove any unsupported policies 260 * 261 * This is used when we have a set of polices that have been requested, but the server 262 * is willing to allow unsupported policies to be considered optional. 263 * 264 * @param policy the polices that were requested 265 * @return the same PolicySet if all are supported; A replacement PolicySet if any 266 * unsupported policies were removed 267 */ 268 public Policy clearUnsupportedPolicies(Policy policy) { 269 // IMPLEMENTATION: At this time, the only policy which might not be supported is 270 // encryption (which requires low-level systems support). Other policies are fully 271 // supported by the framework and do not need to be checked. 272 if (policy.mRequireEncryption) { 273 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 274 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { 275 policy.mRequireEncryption = false; 276 } 277 } 278 279 // If we ever support devices that can't disable cameras for any reason, we should 280 // clear the mDontAllowCamera policy 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 policy 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 policy 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 // There's no user action for changes here; this is just a configuration change 408 reasons |= INACTIVE_NEED_CONFIGURATION; 409 } 410 } 411 if (policy.mPasswordComplexChars > 0) { 412 if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) { 413 reasons |= INACTIVE_NEED_PASSWORD; 414 } 415 } 416 if (policy.mRequireEncryption) { 417 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 418 if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { 419 reasons |= INACTIVE_NEED_ENCRYPTION; 420 } 421 } 422 if (policy.mDontAllowCamera && !dpm.getCameraDisabled(mAdminName)) { 423 reasons |= INACTIVE_NEED_CONFIGURATION; 424 } 425 // password failures are counted locally - no test required here 426 // no check required for remote wipe (it's supported, if we're the admin) 427 428 // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances. 429 return reasons; 430 } 431 // return false, not active 432 return INACTIVE_NEED_ACTIVATION; 433 } 434 435 /** 436 * Set the requested security level based on the aggregate set of requests. 437 * If the set is empty, we release our device administration. If the set is non-empty, 438 * we only proceed if we are already active as an admin. 439 */ 440 public void setActivePolicies() { 441 DevicePolicyManager dpm = getDPM(); 442 // compute aggregate set of policies 443 Policy aggregatePolicy = getAggregatePolicy(); 444 // if empty set, detach from policy manager 445 if (aggregatePolicy == Policy.NO_POLICY) { 446 if (Email.DEBUG) { 447 Log.d(TAG, "setActivePolicies: none, remove admin"); 448 } 449 dpm.removeActiveAdmin(mAdminName); 450 } else if (isActiveAdmin()) { 451 if (Email.DEBUG) { 452 Log.d(TAG, "setActivePolicies: " + aggregatePolicy); 453 } 454 // set each policy in the policy manager 455 // password mode & length 456 dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality()); 457 dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength); 458 // screen lock time 459 dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000); 460 // local wipe (failed passwords limit) 461 dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails); 462 // password expiration (days until a password expires). API takes mSec. 463 long oldExpiration = dpm.getPasswordExpirationTimeout(mAdminName); 464 long newExpiration = aggregatePolicy.getDPManagerPasswordExpirationTimeout(); 465 // we only set this if it has changed; otherwise, we're pushing out the existing 466 // expiration time! 467 if (oldExpiration != newExpiration) { 468 dpm.setPasswordExpirationTimeout(mAdminName, newExpiration); 469 } 470 // password history length (number of previous passwords that may not be reused) 471 dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory); 472 // password minimum complex characters. 473 // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM, 474 // setting the quality to complex also defaults min symbols=1 and min numeric=1. 475 // We always / safely clear minSymbols & minNumeric to zero (there is no policy 476 // configuration in which we explicitly require a minimum number of digits or symbols.) 477 dpm.setPasswordMinimumSymbols(mAdminName, 0); 478 dpm.setPasswordMinimumNumeric(mAdminName, 0); 479 dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars); 480 // Device capabilities 481 dpm.setCameraDisabled(mAdminName, aggregatePolicy.mDontAllowCamera); 482 483 // encryption required 484 dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption); 485 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 486 // Disable/re-enable keyguard features as required 487 boolean noKeyguardFeatures = 488 aggregatePolicy.mPasswordMode != Policy.PASSWORD_MODE_NONE; 489 dpm.setKeyguardDisabledFeatures(mAdminName, 490 (noKeyguardFeatures ? DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL : 491 DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE)); 492 } 493 494 } 495 } 496 497 /** 498 * Convenience method; see javadoc below 499 */ 500 public static void setAccountHoldFlag(Context context, long accountId, boolean newState) { 501 Account account = Account.restoreAccountWithId(context, accountId); 502 if (account != null) { 503 setAccountHoldFlag(context, account, newState); 504 } 505 } 506 507 /** 508 * API: Set/Clear the "hold" flag in any account. This flag serves a dual purpose: 509 * Setting it gives us an indication that it was blocked, and clearing it gives EAS a 510 * signal to try syncing again. 511 * @param context 512 * @param account the account whose hold flag is to be set/cleared 513 * @param newState true = security hold, false = free to sync 514 */ 515 public static void setAccountHoldFlag(Context context, Account account, boolean newState) { 516 if (newState) { 517 account.mFlags |= Account.FLAGS_SECURITY_HOLD; 518 } else { 519 account.mFlags &= ~Account.FLAGS_SECURITY_HOLD; 520 } 521 ContentValues cv = new ContentValues(); 522 cv.put(AccountColumns.FLAGS, account.mFlags); 523 account.update(context, cv); 524 } 525 526 public static void clearAccountPolicy(Context context, Account account) { 527 setAccountPolicy(context, account, null, null); 528 } 529 530 /** 531 * Set the policy for an account atomically; this also removes any other policy associated with 532 * the account and sets the policy key for the account. If policy is null, the policyKey is 533 * set to 0 and the securitySyncKey to null. Also, update the account object to reflect the 534 * current policyKey and securitySyncKey 535 * @param context the caller's context 536 * @param account the account whose policy is to be set 537 * @param policy the policy to set, or null if we're clearing the policy 538 * @param securitySyncKey the security sync key for this account (ignored if policy is null) 539 */ 540 public static void setAccountPolicy(Context context, Account account, Policy policy, 541 String securitySyncKey) { 542 ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); 543 544 // Make sure this is a valid policy set 545 if (policy != null) { 546 policy.normalize(); 547 // Add the new policy (no account will yet reference this) 548 ops.add(ContentProviderOperation.newInsert( 549 Policy.CONTENT_URI).withValues(policy.toContentValues()).build()); 550 // Make the policyKey of the account our newly created policy, and set the sync key 551 ops.add(ContentProviderOperation.newUpdate( 552 ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) 553 .withValueBackReference(AccountColumns.POLICY_KEY, 0) 554 .withValue(AccountColumns.SECURITY_SYNC_KEY, securitySyncKey) 555 .build()); 556 } else { 557 ops.add(ContentProviderOperation.newUpdate( 558 ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) 559 .withValue(AccountColumns.SECURITY_SYNC_KEY, null) 560 .withValue(AccountColumns.POLICY_KEY, 0) 561 .build()); 562 } 563 564 // Delete the previous policy associated with this account, if any 565 if (account.mPolicyKey > 0) { 566 ops.add(ContentProviderOperation.newDelete( 567 ContentUris.withAppendedId( 568 Policy.CONTENT_URI, account.mPolicyKey)).build()); 569 } 570 571 try { 572 context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); 573 account.refresh(context); 574 } catch (RemoteException e) { 575 // This is fatal to a remote process 576 throw new IllegalStateException("Exception setting account policy."); 577 } catch (OperationApplicationException e) { 578 // Can't happen; our provider doesn't throw this exception 579 } 580 } 581 582 /** 583 * API: Report that policies may have been updated due to rewriting values in an Account; we 584 * clear the aggregate policy (so it can be recomputed) and set the policies in the DPM 585 */ 586 public synchronized void policiesUpdated() { 587 mAggregatePolicy = null; 588 setActivePolicies(); 589 } 590 591 public void setAccountPolicy(long accountId, Policy policy, String securityKey) { 592 Account account = Account.restoreAccountWithId(mContext, accountId); 593 Policy oldPolicy = null; 594 if (account.mPolicyKey > 0) { 595 oldPolicy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); 596 } 597 boolean policyChanged = (oldPolicy == null) || !oldPolicy.equals(policy); 598 if (!policyChanged && (TextUtilities.stringOrNullEquals(securityKey, 599 account.mSecuritySyncKey))) { 600 Log.d(Logging.LOG_TAG, "setAccountPolicy; policy unchanged"); 601 } else { 602 setAccountPolicy(mContext, account, policy, securityKey); 603 policiesUpdated(); 604 } 605 606 boolean setHold = false; 607 if (isActive(policy)) { 608 // For Email1, ignore; it's really just a courtesy notification 609 } else { 610 setHold = true; 611 Log.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName + 612 " are not being enforced."); 613 // Put up a notification 614 NotificationController.getInstance(mContext).showSecurityNeededNotification(account); 615 } 616 // Set/clear the account hold. 617 setAccountHoldFlag(mContext, account, setHold); 618 } 619 620 /** 621 * API: Sync service should call this any time a sync fails due to isActive() returning false. 622 * This will kick off the notify-acquire-admin-state process and/or increase the security level. 623 * The caller needs to write the required policies into this account before making this call. 624 * Should not be called from UI thread - uses DB lookups to prepare new notifications 625 * 626 * @param accountId the account for which sync cannot proceed 627 */ 628 public void policiesRequired(long accountId) { 629 Account account = Account.restoreAccountWithId(mContext, accountId); 630 // In case the account has been deleted, just return 631 if (account == null) return; 632 if (Email.DEBUG) { 633 if (account.mPolicyKey == 0) { 634 Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": none"); 635 } else { 636 Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); 637 if (policy == null) { 638 Log.w(TAG, "No policy??"); 639 } else { 640 Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy); 641 } 642 } 643 } 644 645 // Mark the account as "on hold". 646 setAccountHoldFlag(mContext, account, true); 647 648 // Put up a notification 649 NotificationController.getInstance(mContext).showSecurityNeededNotification(account); 650 } 651 652 /** 653 * Called from the notification's intent receiver to register that the notification can be 654 * cleared now. 655 */ 656 public void clearNotification() { 657 NotificationController.getInstance(mContext).cancelSecurityNeededNotification(); 658 } 659 660 /** 661 * API: Remote wipe (from server). This is final, there is no confirmation. It will only 662 * return to the caller if there is an unexpected failure. The wipe includes external storage. 663 */ 664 public void remoteWipe() { 665 DevicePolicyManager dpm = getDPM(); 666 if (dpm.isAdminActive(mAdminName)) { 667 dpm.wipeData(DevicePolicyManager.WIPE_EXTERNAL_STORAGE); 668 } else { 669 Log.d(Logging.LOG_TAG, "Could not remote wipe because not device admin."); 670 } 671 } 672 /** 673 * If we are not the active device admin, try to become so. 674 * 675 * Also checks for any policies that we have added during the lifetime of this app. 676 * This catches the case where the user granted an earlier (smaller) set of policies 677 * but an app upgrade requires that new policies be granted. 678 * 679 * @return true if we are already active, false if we are not 680 */ 681 public boolean isActiveAdmin() { 682 DevicePolicyManager dpm = getDPM(); 683 return dpm.isAdminActive(mAdminName) 684 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD) 685 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE) 686 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_DISABLE_CAMERA) 687 && dpm.hasGrantedPolicy(mAdminName, 688 DeviceAdminInfo.USES_POLICY_DISABLE_KEYGUARD_FEATURES); 689 } 690 691 /** 692 * Report admin component name - for making calls into device policy manager 693 */ 694 public ComponentName getAdminComponent() { 695 return mAdminName; 696 } 697 698 /** 699 * Delete all accounts whose security flags aren't zero (i.e. they have security enabled). 700 * This method is synchronous, so it should normally be called within a worker thread (the 701 * exception being for unit tests) 702 * 703 * @param context the caller's context 704 */ 705 /*package*/ void deleteSecuredAccounts(Context context) { 706 ContentResolver cr = context.getContentResolver(); 707 // Find all accounts with security and delete them 708 Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, 709 Account.SECURITY_NONZERO_SELECTION, null, null); 710 try { 711 Log.w(TAG, "Email administration disabled; deleting " + c.getCount() + 712 " secured account(s)"); 713 while (c.moveToNext()) { 714 Controller.getInstance(context).deleteAccountSync( 715 c.getLong(EmailContent.ID_PROJECTION_COLUMN), context); 716 } 717 } finally { 718 c.close(); 719 } 720 policiesUpdated(-1); 721 } 722 723 /** 724 * Internal handler for enabled->disabled transitions. Deletes all secured accounts. 725 * Must call from worker thread, not on UI thread. 726 */ 727 /*package*/ void onAdminEnabled(boolean isEnabled) { 728 if (!isEnabled) { 729 deleteSecuredAccounts(mContext); 730 } 731 } 732 733 /** 734 * Handle password expiration - if any accounts appear to have triggered this, put up 735 * warnings, or even shut them down. 736 * 737 * NOTE: If there are multiple accounts with password expiration policies, the device 738 * password will be set to expire in the shortest required interval (most secure). The logic 739 * in this method operates based on the aggregate setting - irrespective of which account caused 740 * the expiration. In other words, all accounts (that require expiration) will run/stop 741 * based on the requirements of the account with the shortest interval. 742 */ 743 private void onPasswordExpiring(Context context) { 744 // 1. Do we have any accounts that matter here? 745 long nextExpiringAccountId = findShortestExpiration(context); 746 747 // 2. If not, exit immediately 748 if (nextExpiringAccountId == -1) { 749 return; 750 } 751 752 // 3. If yes, are we warning or expired? 753 long expirationDate = getDPM().getPasswordExpiration(mAdminName); 754 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 755 boolean expired = timeUntilExpiration < 0; 756 if (!expired) { 757 // 4. If warning, simply put up a generic notification and report that it came from 758 // the shortest-expiring account. 759 NotificationController.getInstance(mContext).showPasswordExpiringNotification( 760 nextExpiringAccountId); 761 } else { 762 // 5. Actually expired - find all accounts that expire passwords, and wipe them 763 boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context)); 764 if (wiped) { 765 NotificationController.getInstance(mContext).showPasswordExpiredNotification( 766 nextExpiringAccountId); 767 } 768 } 769 } 770 771 /** 772 * Find the account with the shortest expiration time. This is always assumed to be 773 * the account that forces the password to be refreshed. 774 * @return -1 if no expirations, or accountId if one is found 775 */ 776 @VisibleForTesting 777 /*package*/ static long findShortestExpiration(Context context) { 778 long policyId = Utility.getFirstRowLong(context, Policy.CONTENT_URI, Policy.ID_PROJECTION, 779 HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC", 780 EmailContent.ID_PROJECTION_COLUMN, -1L); 781 if (policyId < 0) return -1L; 782 return Policy.getAccountIdWithPolicyKey(context, policyId); 783 } 784 785 /** 786 * For all accounts that require password expiration, put them in security hold and wipe 787 * their data. 788 * @param context 789 * @param controller 790 * @return true if one or more accounts were wiped 791 */ 792 @VisibleForTesting 793 /*package*/ static boolean wipeExpiredAccounts(Context context, Controller controller) { 794 boolean result = false; 795 Cursor c = context.getContentResolver().query(Policy.CONTENT_URI, 796 Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null); 797 try { 798 while (c.moveToNext()) { 799 long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN); 800 long accountId = Policy.getAccountIdWithPolicyKey(context, policyId); 801 if (accountId < 0) continue; 802 Account account = Account.restoreAccountWithId(context, accountId); 803 if (account != null) { 804 // Mark the account as "on hold". 805 setAccountHoldFlag(context, account, true); 806 // Erase data 807 controller.deleteSyncedDataSync(accountId); 808 // Report one or more were found 809 result = true; 810 } 811 } 812 } finally { 813 c.close(); 814 } 815 return result; 816 } 817 818 /** 819 * Callback from EmailBroadcastProcessorService. This provides the workers for the 820 * DeviceAdminReceiver calls. These should perform the work directly and not use async 821 * threads for completion. 822 */ 823 public static void onDeviceAdminReceiverMessage(Context context, int message) { 824 SecurityPolicy instance = SecurityPolicy.getInstance(context); 825 switch (message) { 826 case DEVICE_ADMIN_MESSAGE_ENABLED: 827 instance.onAdminEnabled(true); 828 break; 829 case DEVICE_ADMIN_MESSAGE_DISABLED: 830 instance.onAdminEnabled(false); 831 break; 832 case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED: 833 // TODO make a small helper for this 834 // Clear security holds (if any) 835 Account.clearSecurityHoldOnAllAccounts(context); 836 // Cancel any active notifications (if any are posted) 837 NotificationController.getInstance(context).cancelPasswordExpirationNotifications(); 838 break; 839 case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING: 840 instance.onPasswordExpiring(instance.mContext); 841 break; 842 } 843 } 844 845 /** 846 * Device Policy administrator. This is primarily a listener for device state changes. 847 * Note: This is instantiated by incoming messages. 848 * Note: This is actually a BroadcastReceiver and must remain within the guidelines required 849 * for proper behavior, including avoidance of ANRs. 850 * Note: We do not implement onPasswordFailed() because the default behavior of the 851 * DevicePolicyManager - complete local wipe after 'n' failures - is sufficient. 852 */ 853 public static class PolicyAdmin extends DeviceAdminReceiver { 854 855 /** 856 * Called after the administrator is first enabled. 857 */ 858 @Override 859 public void onEnabled(Context context, Intent intent) { 860 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 861 DEVICE_ADMIN_MESSAGE_ENABLED); 862 } 863 864 /** 865 * Called prior to the administrator being disabled. 866 */ 867 @Override 868 public void onDisabled(Context context, Intent intent) { 869 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 870 DEVICE_ADMIN_MESSAGE_DISABLED); 871 } 872 873 /** 874 * Called when the user asks to disable administration; we return a warning string that 875 * will be presented to the user 876 */ 877 @Override 878 public CharSequence onDisableRequested(Context context, Intent intent) { 879 return context.getString(R.string.disable_admin_warning); 880 } 881 882 /** 883 * Called after the user has changed their password. 884 */ 885 @Override 886 public void onPasswordChanged(Context context, Intent intent) { 887 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 888 DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED); 889 } 890 891 /** 892 * Called when device password is expiring 893 */ 894 @Override 895 public void onPasswordExpiring(Context context, Intent intent) { 896 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 897 DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING); 898 } 899 } 900} 901