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