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