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