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