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