SecurityPolicy.java revision 844b14f851ce748b7b204125264bb3343c1a9039
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 17package com.android.email; 18 19import com.android.email.activity.setup.AccountSecurity; 20import com.android.email.provider.EmailContent; 21import com.android.email.provider.EmailContent.Account; 22import com.android.email.provider.EmailContent.AccountColumns; 23import com.android.email.service.EmailBroadcastProcessorService; 24 25import android.app.admin.DeviceAdminInfo; 26import android.app.admin.DeviceAdminReceiver; 27import android.app.admin.DevicePolicyManager; 28import android.content.ComponentName; 29import android.content.ContentResolver; 30import android.content.ContentValues; 31import android.content.Context; 32import android.content.Intent; 33import android.database.Cursor; 34import android.os.Parcel; 35import android.os.Parcelable; 36import android.util.Log; 37 38/** 39 * Utility functions to support reading and writing security policies, and handshaking the device 40 * into and out of various security states. 41 */ 42public class SecurityPolicy { 43 private static final String TAG = "SecurityPolicy"; 44 private static SecurityPolicy sInstance = null; 45 private Context mContext; 46 private DevicePolicyManager mDPM; 47 private ComponentName mAdminName; 48 private PolicySet mAggregatePolicy; 49 50 /* package */ static final PolicySet NO_POLICY_SET = 51 new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, false); 52 53 /** 54 * This projection on Account is for scanning/reading 55 */ 56 private static final String[] ACCOUNT_SECURITY_PROJECTION = new String[] { 57 AccountColumns.ID, AccountColumns.SECURITY_FLAGS 58 }; 59 private static final int ACCOUNT_SECURITY_COLUMN_ID = 0; 60 private static final int ACCOUNT_SECURITY_COLUMN_FLAGS = 1; 61 62 // Messages used for DevicePolicyManager callbacks 63 private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1; 64 private static final int DEVICE_ADMIN_MESSAGE_DISABLED = 2; 65 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3; 66 private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4; 67 68 /** 69 * Get the security policy instance 70 */ 71 public synchronized static SecurityPolicy getInstance(Context context) { 72 if (sInstance == null) { 73 sInstance = new SecurityPolicy(context.getApplicationContext()); 74 } 75 return sInstance; 76 } 77 78 /** 79 * Private constructor (one time only) 80 */ 81 private SecurityPolicy(Context context) { 82 mContext = context.getApplicationContext(); 83 mDPM = null; 84 mAdminName = new ComponentName(context, PolicyAdmin.class); 85 mAggregatePolicy = null; 86 } 87 88 /** 89 * For testing only: Inject context into already-created instance 90 */ 91 /* package */ void setContext(Context context) { 92 mContext = context; 93 } 94 95 /** 96 * Compute the aggregate policy for all accounts that require it, and record it. 97 * 98 * The business logic is as follows: 99 * min password length take the max 100 * password mode take the max (strongest mode) 101 * max password fails take the min 102 * max screen lock time take the min 103 * require remote wipe take the max (logical or) 104 * password history take the max (strongest mode) 105 * password expiration take the min (strongest mode) 106 * password complex chars take the max (strongest mode) 107 * encryption take the max (logical or) 108 * 109 * @return a policy representing the strongest aggregate. If no policy sets are defined, 110 * a lightweight "nothing required" policy will be returned. Never null. 111 */ 112 /*package*/ PolicySet computeAggregatePolicy() { 113 boolean policiesFound = false; 114 115 int minPasswordLength = Integer.MIN_VALUE; 116 int passwordMode = Integer.MIN_VALUE; 117 int maxPasswordFails = Integer.MAX_VALUE; 118 int maxScreenLockTime = Integer.MAX_VALUE; 119 boolean requireRemoteWipe = false; 120 int passwordHistory = Integer.MIN_VALUE; 121 int passwordExpirationDays = Integer.MAX_VALUE; 122 int passwordComplexChars = Integer.MIN_VALUE; 123 boolean requireEncryption = false; 124 125 Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI, 126 ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); 127 try { 128 while (c.moveToNext()) { 129 long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); 130 if (flags != 0) { 131 PolicySet p = new PolicySet(flags); 132 minPasswordLength = Math.max(p.mMinPasswordLength, minPasswordLength); 133 passwordMode = Math.max(p.mPasswordMode, passwordMode); 134 if (p.mMaxPasswordFails > 0) { 135 maxPasswordFails = Math.min(p.mMaxPasswordFails, maxPasswordFails); 136 } 137 if (p.mMaxScreenLockTime > 0) { 138 maxScreenLockTime = Math.min(p.mMaxScreenLockTime, maxScreenLockTime); 139 } 140 if (p.mPasswordHistory > 0) { 141 passwordHistory = Math.max(p.mPasswordHistory, passwordHistory); 142 } 143 if (p.mPasswordExpirationDays > 0) { 144 passwordExpirationDays = 145 Math.min(p.mPasswordExpirationDays, passwordExpirationDays); 146 } 147 if (p.mPasswordComplexChars > 0) { 148 passwordComplexChars = Math.max(p.mPasswordComplexChars, 149 passwordComplexChars); 150 } 151 requireRemoteWipe |= p.mRequireRemoteWipe; 152 requireEncryption |= p.mRequireEncryption; 153 policiesFound = true; 154 } 155 } 156 } finally { 157 c.close(); 158 } 159 if (policiesFound) { 160 // final cleanup pass converts any untouched min/max values to zero (not specified) 161 if (minPasswordLength == Integer.MIN_VALUE) minPasswordLength = 0; 162 if (passwordMode == Integer.MIN_VALUE) passwordMode = 0; 163 if (maxPasswordFails == Integer.MAX_VALUE) maxPasswordFails = 0; 164 if (maxScreenLockTime == Integer.MAX_VALUE) maxScreenLockTime = 0; 165 if (passwordHistory == Integer.MIN_VALUE) passwordHistory = 0; 166 if (passwordExpirationDays == Integer.MAX_VALUE) passwordExpirationDays = 0; 167 if (passwordComplexChars == Integer.MIN_VALUE) passwordComplexChars = 0; 168 169 return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails, 170 maxScreenLockTime, requireRemoteWipe, passwordExpirationDays, passwordHistory, 171 passwordComplexChars, requireEncryption); 172 } else { 173 return NO_POLICY_SET; 174 } 175 } 176 177 /** 178 * Return updated aggregate policy, from cached value if possible 179 */ 180 public synchronized PolicySet getAggregatePolicy() { 181 if (mAggregatePolicy == null) { 182 mAggregatePolicy = computeAggregatePolicy(); 183 } 184 return mAggregatePolicy; 185 } 186 187 /** 188 * Get the dpm. This mainly allows us to make some utility calls without it, for testing. 189 */ 190 /* package */ synchronized DevicePolicyManager getDPM() { 191 if (mDPM == null) { 192 mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); 193 } 194 return mDPM; 195 } 196 197 /** 198 * API: Report that policies may have been updated due to rewriting values in an Account. 199 * @param accountId the account that has been updated, -1 if unknown/deleted 200 */ 201 public synchronized void updatePolicies(long accountId) { 202 mAggregatePolicy = null; 203 } 204 205 /** 206 * API: Report that policies may have been updated *and* the caller vouches that the 207 * change is a reduction in policies. This forces an immediate change to device state. 208 * Typically used when deleting accounts, although we may use it for server-side policy 209 * rollbacks. 210 */ 211 public void reducePolicies() { 212 updatePolicies(-1); 213 setActivePolicies(); 214 } 215 216 /** 217 * API: Query if the proposed set of policies are supported on the device. 218 * 219 * @param policies the polices that were requested 220 * @return boolean if supported 221 */ 222 public boolean isSupported(PolicySet policies) { 223 // IMPLEMENTATION: At this time, the only policy which might not be supported is 224 // encryption (which requires low-level systems support). Other policies are fully 225 // supported by the framework and do not need to be checked. 226 if (policies.mRequireEncryption) { 227 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 228 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { 229 return false; 230 } 231 } 232 return true; 233 } 234 235 /** 236 * API: Remove any unsupported policies 237 * 238 * This is used when we have a set of polices that have been requested, but the server 239 * is willing to allow unsupported policies to be considered optional. 240 * 241 * @param policies the polices that were requested 242 * @return the same PolicySet if all are supported; A replacement PolicySet if any 243 * unsupported policies were removed 244 */ 245 public PolicySet clearUnsupportedPolicies(PolicySet policies) { 246 PolicySet result = policies; 247 // IMPLEMENTATION: At this time, the only policy which might not be supported is 248 // encryption (which requires low-level systems support). Other policies are fully 249 // supported by the framework and do not need to be checked. 250 if (policies.mRequireEncryption) { 251 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 252 if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { 253 // Make new PolicySet w/o encryption 254 result = new PolicySet(policies.mMinPasswordLength, policies.mPasswordMode, 255 policies.mMaxPasswordFails, policies.mMaxScreenLockTime, 256 policies.mRequireRemoteWipe, policies.mPasswordExpirationDays, 257 policies.mPasswordHistory, policies.mPasswordComplexChars, false); 258 } 259 } 260 return result; 261 } 262 263 /** 264 * API: Query used to determine if a given policy is "active" (the device is operating at 265 * the required security level). 266 * 267 * @param policies the policies requested, or null to check aggregate stored policies 268 * @return true if the requested policies are active, false if not. 269 */ 270 public boolean isActive(PolicySet policies) { 271 int reasons = getInactiveReasons(policies); 272 return reasons == 0; 273 } 274 275 /** 276 * Return bits from isActive: Device Policy Manager has not been activated 277 */ 278 public final static int INACTIVE_NEED_ACTIVATION = 1; 279 280 /** 281 * Return bits from isActive: Some required configuration is not correct (no user action). 282 */ 283 public final static int INACTIVE_NEED_CONFIGURATION = 2; 284 285 /** 286 * Return bits from isActive: Password needs to be set or updated 287 */ 288 public final static int INACTIVE_NEED_PASSWORD = 4; 289 290 /** 291 * Return bits from isActive: Encryption has not be enabled 292 */ 293 public final static int INACTIVE_NEED_ENCRYPTION = 8; 294 295 /** 296 * API: Query used to determine if a given policy is "active" (the device is operating at 297 * the required security level). 298 * 299 * This can be used when syncing a specific account, by passing a specific set of policies 300 * for that account. Or, it can be used at any time to compare the device 301 * state against the aggregate set of device policies stored in all accounts. 302 * 303 * This method is for queries only, and does not trigger any change in device state. 304 * 305 * NOTE: If there are multiple accounts with password expiration policies, the device 306 * password will be set to expire in the shortest required interval (most secure). This method 307 * will return 'false' as soon as the password expires - irrespective of which account caused 308 * the expiration. In other words, all accounts (that require expiration) will run/stop 309 * based on the requirements of the account with the shortest interval. 310 * 311 * @param policies the policies requested, or null to check aggregate stored policies 312 * @return zero if the requested policies are active, non-zero bits indicates that more work 313 * is needed (typically, by the user) before the required security polices are fully active. 314 */ 315 public int getInactiveReasons(PolicySet policies) { 316 // select aggregate set if needed 317 if (policies == null) { 318 policies = getAggregatePolicy(); 319 } 320 // quick check for the "empty set" of no policies 321 if (policies == NO_POLICY_SET) { 322 return 0; 323 } 324 int reasons = 0; 325 DevicePolicyManager dpm = getDPM(); 326 if (isActiveAdmin()) { 327 // check each policy explicitly 328 if (policies.mMinPasswordLength > 0) { 329 if (dpm.getPasswordMinimumLength(mAdminName) < policies.mMinPasswordLength) { 330 reasons |= INACTIVE_NEED_PASSWORD; 331 } 332 } 333 if (policies.mPasswordMode > 0) { 334 if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) { 335 reasons |= INACTIVE_NEED_PASSWORD; 336 } 337 if (!dpm.isActivePasswordSufficient()) { 338 reasons |= INACTIVE_NEED_PASSWORD; 339 } 340 } 341 if (policies.mMaxScreenLockTime > 0) { 342 // Note, we use seconds, dpm uses milliseconds 343 if (dpm.getMaximumTimeToLock(mAdminName) > policies.mMaxScreenLockTime * 1000) { 344 reasons |= INACTIVE_NEED_CONFIGURATION; 345 } 346 } 347 if (policies.mPasswordExpirationDays > 0) { 348 // confirm that expirations are currently set 349 long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName); 350 if (currentTimeout == 0 351 || currentTimeout > policies.getDPManagerPasswordExpirationTimeout()) { 352 reasons |= INACTIVE_NEED_PASSWORD; 353 } 354 // confirm that the current password hasn't expired 355 long expirationDate = dpm.getPasswordExpiration(mAdminName); 356 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 357 boolean expired = timeUntilExpiration < 0; 358 if (expired) { 359 reasons |= INACTIVE_NEED_PASSWORD; 360 } 361 } 362 if (policies.mPasswordHistory > 0) { 363 if (dpm.getPasswordHistoryLength(mAdminName) < policies.mPasswordHistory) { 364 reasons |= INACTIVE_NEED_PASSWORD; 365 } 366 } 367 if (policies.mPasswordComplexChars > 0) { 368 if (dpm.getPasswordMinimumNonLetter(mAdminName) < policies.mPasswordComplexChars) { 369 reasons |= INACTIVE_NEED_PASSWORD; 370 } 371 } 372 if (policies.mRequireEncryption) { 373 int encryptionStatus = getDPM().getStorageEncryptionStatus(); 374 if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { 375 reasons |= INACTIVE_NEED_ENCRYPTION; 376 } 377 } 378 // password failures are counted locally - no test required here 379 // no check required for remote wipe (it's supported, if we're the admin) 380 381 // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances. 382 return reasons; 383 } 384 // return false, not active 385 return INACTIVE_NEED_ACTIVATION; 386 } 387 388 /** 389 * Set the requested security level based on the aggregate set of requests. 390 * If the set is empty, we release our device administration. If the set is non-empty, 391 * we only proceed if we are already active as an admin. 392 */ 393 public void setActivePolicies() { 394 DevicePolicyManager dpm = getDPM(); 395 // compute aggregate set of policies 396 PolicySet policies = getAggregatePolicy(); 397 // if empty set, detach from policy manager 398 if (policies == NO_POLICY_SET) { 399 dpm.removeActiveAdmin(mAdminName); 400 } else if (isActiveAdmin()) { 401 // set each policy in the policy manager 402 // password mode & length 403 dpm.setPasswordQuality(mAdminName, policies.getDPManagerPasswordQuality()); 404 dpm.setPasswordMinimumLength(mAdminName, policies.mMinPasswordLength); 405 // screen lock time 406 dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000); 407 // local wipe (failed passwords limit) 408 dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails); 409 // password expiration (days until a password expires). API takes mSec. 410 dpm.setPasswordExpirationTimeout(mAdminName, 411 policies.getDPManagerPasswordExpirationTimeout()); 412 // password history length (number of previous passwords that may not be reused) 413 dpm.setPasswordHistoryLength(mAdminName, policies.mPasswordHistory); 414 // password minimum complex characters 415 dpm.setPasswordMinimumNonLetter(mAdminName, policies.mPasswordComplexChars); 416 // encryption required 417 dpm.setStorageEncryption(mAdminName, policies.mRequireEncryption); 418 } 419 } 420 421 /** 422 * API: Set/Clear the "hold" flag in any account. This flag serves a dual purpose: 423 * Setting it gives us an indication that it was blocked, and clearing it gives EAS a 424 * signal to try syncing again. 425 * @param context 426 * @param account The account to update 427 * @param newState true = security hold, false = free to sync 428 */ 429 public static void setAccountHoldFlag(Context context, Account account, boolean newState) { 430 if (newState) { 431 account.mFlags |= Account.FLAGS_SECURITY_HOLD; 432 } else { 433 account.mFlags &= ~Account.FLAGS_SECURITY_HOLD; 434 } 435 ContentValues cv = new ContentValues(); 436 cv.put(AccountColumns.FLAGS, account.mFlags); 437 account.update(context, cv); 438 } 439 440 /** 441 * API: Sync service should call this any time a sync fails due to isActive() returning false. 442 * This will kick off the notify-acquire-admin-state process and/or increase the security level. 443 * The caller needs to write the required policies into this account before making this call. 444 * Should not be called from UI thread - uses DB lookups to prepare new notifications 445 * 446 * @param accountId the account for which sync cannot proceed 447 */ 448 public void policiesRequired(long accountId) { 449 Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId); 450 // In case the account has been deleted, just return 451 if (account == null) return; 452 453 // Mark the account as "on hold". 454 setAccountHoldFlag(mContext, account, true); 455 456 // Put up a notification 457 String tickerText = mContext.getString(R.string.security_notification_ticker_fmt, 458 account.getDisplayName()); 459 String contentTitle = mContext.getString(R.string.security_notification_content_title); 460 String contentText = account.getDisplayName(); 461 Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, accountId); 462 NotificationController.getInstance(mContext).postAccountNotification( 463 account, tickerText, contentTitle, contentText, intent, 464 NotificationController.NOTIFICATION_ID_SECURITY_NEEDED); 465 } 466 467 /** 468 * Called from the notification's intent receiver to register that the notification can be 469 * cleared now. 470 */ 471 public void clearNotification(long accountId) { 472 NotificationController.getInstance(mContext).cancelNotification( 473 NotificationController.NOTIFICATION_ID_SECURITY_NEEDED); 474 } 475 476 /** 477 * API: Remote wipe (from server). This is final, there is no confirmation. It will only 478 * return to the caller if there is an unexpected failure. 479 */ 480 public void remoteWipe() { 481 DevicePolicyManager dpm = getDPM(); 482 if (dpm.isAdminActive(mAdminName)) { 483 dpm.wipeData(0); 484 } else { 485 Log.d(Email.LOG_TAG, "Could not remote wipe because not device admin."); 486 } 487 } 488 489 /** 490 * Class for tracking policies and reading/writing into accounts 491 */ 492 public static class PolicySet implements Parcelable { 493 494 // Security (provisioning) flags 495 // bits 0..4: password length (0=no password required) 496 private static final int PASSWORD_LENGTH_MASK = 31; 497 private static final int PASSWORD_LENGTH_SHIFT = 0; 498 public static final int PASSWORD_LENGTH_MAX = 30; 499 // bits 5..8: password mode 500 private static final int PASSWORD_MODE_SHIFT = 5; 501 private static final int PASSWORD_MODE_MASK = 15 << PASSWORD_MODE_SHIFT; 502 public static final int PASSWORD_MODE_NONE = 0 << PASSWORD_MODE_SHIFT; 503 public static final int PASSWORD_MODE_SIMPLE = 1 << PASSWORD_MODE_SHIFT; 504 public static final int PASSWORD_MODE_STRONG = 2 << PASSWORD_MODE_SHIFT; 505 // bits 9..13: password failures -> wipe device (0=disabled) 506 private static final int PASSWORD_MAX_FAILS_SHIFT = 9; 507 private static final int PASSWORD_MAX_FAILS_MASK = 31 << PASSWORD_MAX_FAILS_SHIFT; 508 public static final int PASSWORD_MAX_FAILS_MAX = 31; 509 // bits 14..24: seconds to screen lock (0=not required) 510 private static final int SCREEN_LOCK_TIME_SHIFT = 14; 511 private static final int SCREEN_LOCK_TIME_MASK = 2047 << SCREEN_LOCK_TIME_SHIFT; 512 public static final int SCREEN_LOCK_TIME_MAX = 2047; 513 // bit 25: remote wipe capability required 514 private static final int REQUIRE_REMOTE_WIPE = 1 << 25; 515 // bit 26..35: password expiration (days; 0=not required) 516 private static final int PASSWORD_EXPIRATION_SHIFT = 26; 517 private static final long PASSWORD_EXPIRATION_MASK = 1023L << PASSWORD_EXPIRATION_SHIFT; 518 public static final int PASSWORD_EXPIRATION_MAX = 1023; 519 // bit 36..43: password history (length; 0=not required) 520 private static final int PASSWORD_HISTORY_SHIFT = 36; 521 private static final long PASSWORD_HISTORY_MASK = 255L << PASSWORD_HISTORY_SHIFT; 522 public static final int PASSWORD_HISTORY_MAX = 255; 523 // bit 44..48: min complex characters (0=not required) 524 private static final int PASSWORD_COMPLEX_CHARS_SHIFT = 44; 525 private static final long PASSWORD_COMPLEX_CHARS_MASK = 31L << PASSWORD_COMPLEX_CHARS_SHIFT; 526 public static final int PASSWORD_COMPLEX_CHARS_MAX = 31; 527 // bit 49: requires device encryption 528 private static final long REQUIRE_ENCRYPTION = 1L << 49; 529 530 /* Convert days to mSec (used for password expiration) */ 531 private static final long DAYS_TO_MSEC = 24 * 60 * 60 * 1000; 532 /* Small offset (2 minutes) added to policy expiration to make user testing easier. */ 533 private static final long EXPIRATION_OFFSET_MSEC = 2 * 60 * 1000; 534 535 /*package*/ final int mMinPasswordLength; 536 /*package*/ final int mPasswordMode; 537 /*package*/ final int mMaxPasswordFails; 538 /*package*/ final int mMaxScreenLockTime; 539 /*package*/ final boolean mRequireRemoteWipe; 540 /*package*/ final int mPasswordExpirationDays; 541 /*package*/ final int mPasswordHistory; 542 /*package*/ final int mPasswordComplexChars; 543 /*package*/ final boolean mRequireEncryption; 544 545 public int getMinPasswordLengthForTest() { 546 return mMinPasswordLength; 547 } 548 549 public int getPasswordModeForTest() { 550 return mPasswordMode; 551 } 552 553 public int getMaxPasswordFailsForTest() { 554 return mMaxPasswordFails; 555 } 556 557 public int getMaxScreenLockTimeForTest() { 558 return mMaxScreenLockTime; 559 } 560 561 public boolean isRequireRemoteWipeForTest() { 562 return mRequireRemoteWipe; 563 } 564 565 public boolean isRequireEncryptionForTest() { 566 return mRequireEncryption; 567 } 568 569 /** 570 * Create from raw values. 571 * @param minPasswordLength (0=not enforced) 572 * @param passwordMode 573 * @param maxPasswordFails (0=not enforced) 574 * @param maxScreenLockTime in seconds (0=not enforced) 575 * @param requireRemoteWipe 576 * @param passwordExpirationDays in days (0=not enforced) 577 * @param passwordHistory (0=not enforced) 578 * @param passwordComplexChars (0=not enforced) 579 * @throws IllegalArgumentException for illegal arguments. 580 */ 581 public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails, 582 int maxScreenLockTime, boolean requireRemoteWipe, int passwordExpirationDays, 583 int passwordHistory, int passwordComplexChars, boolean requireEncryption) 584 throws IllegalArgumentException { 585 // If we're not enforcing passwords, make sure we clean up related values, since EAS 586 // can send non-zero values for any or all of these 587 if (passwordMode == PASSWORD_MODE_NONE) { 588 maxPasswordFails = 0; 589 maxScreenLockTime = 0; 590 minPasswordLength = 0; 591 passwordComplexChars = 0; 592 passwordHistory = 0; 593 passwordExpirationDays = 0; 594 } else { 595 if ((passwordMode != PASSWORD_MODE_SIMPLE) && 596 (passwordMode != PASSWORD_MODE_STRONG)) { 597 throw new IllegalArgumentException("password mode"); 598 } 599 // If we're only requiring a simple password, set complex chars to zero; note 600 // that EAS can erroneously send non-zero values in this case 601 if (passwordMode == PASSWORD_MODE_SIMPLE) { 602 passwordComplexChars = 0; 603 } 604 // The next four values have hard limits which cannot be supported if exceeded. 605 if (minPasswordLength > PASSWORD_LENGTH_MAX) { 606 throw new IllegalArgumentException("password length"); 607 } 608 if (passwordExpirationDays > PASSWORD_EXPIRATION_MAX) { 609 throw new IllegalArgumentException("password expiration"); 610 } 611 if (passwordHistory > PASSWORD_HISTORY_MAX) { 612 throw new IllegalArgumentException("password history"); 613 } 614 if (passwordComplexChars > PASSWORD_COMPLEX_CHARS_MAX) { 615 throw new IllegalArgumentException("complex chars"); 616 } 617 // This value can be reduced (which actually increases security) if necessary 618 if (maxPasswordFails > PASSWORD_MAX_FAILS_MAX) { 619 maxPasswordFails = PASSWORD_MAX_FAILS_MAX; 620 } 621 // This value can be reduced (which actually increases security) if necessary 622 if (maxScreenLockTime > SCREEN_LOCK_TIME_MAX) { 623 maxScreenLockTime = SCREEN_LOCK_TIME_MAX; 624 } 625 } 626 mMinPasswordLength = minPasswordLength; 627 mPasswordMode = passwordMode; 628 mMaxPasswordFails = maxPasswordFails; 629 mMaxScreenLockTime = maxScreenLockTime; 630 mRequireRemoteWipe = requireRemoteWipe; 631 mPasswordExpirationDays = passwordExpirationDays; 632 mPasswordHistory = passwordHistory; 633 mPasswordComplexChars = passwordComplexChars; 634 mRequireEncryption = requireEncryption; 635 } 636 637 /** 638 * Create from values encoded in an account 639 * @param account 640 */ 641 public PolicySet(Account account) { 642 this(account.mSecurityFlags); 643 } 644 645 /** 646 * Create from values encoded in an account flags int 647 */ 648 public PolicySet(long flags) { 649 mMinPasswordLength = 650 (int) ((flags & PASSWORD_LENGTH_MASK) >> PASSWORD_LENGTH_SHIFT); 651 mPasswordMode = 652 (int) (flags & PASSWORD_MODE_MASK); 653 mMaxPasswordFails = 654 (int) ((flags & PASSWORD_MAX_FAILS_MASK) >> PASSWORD_MAX_FAILS_SHIFT); 655 mMaxScreenLockTime = 656 (int) ((flags & SCREEN_LOCK_TIME_MASK) >> SCREEN_LOCK_TIME_SHIFT); 657 mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE); 658 mPasswordExpirationDays = 659 (int) ((flags & PASSWORD_EXPIRATION_MASK) >> PASSWORD_EXPIRATION_SHIFT); 660 mPasswordHistory = 661 (int) ((flags & PASSWORD_HISTORY_MASK) >> PASSWORD_HISTORY_SHIFT); 662 mPasswordComplexChars = 663 (int) ((flags & PASSWORD_COMPLEX_CHARS_MASK) >> PASSWORD_COMPLEX_CHARS_SHIFT); 664 mRequireEncryption = 0 != (flags & REQUIRE_ENCRYPTION); 665 } 666 667 /** 668 * Helper to map our internal encoding to DevicePolicyManager password modes. 669 */ 670 public int getDPManagerPasswordQuality() { 671 switch (mPasswordMode) { 672 case PASSWORD_MODE_SIMPLE: 673 return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; 674 case PASSWORD_MODE_STRONG: 675 return DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; 676 default: 677 return DevicePolicyManager .PASSWORD_QUALITY_UNSPECIFIED; 678 } 679 } 680 681 /** 682 * Helper to map expiration times to the millisecond values used by DevicePolicyManager. 683 */ 684 public long getDPManagerPasswordExpirationTimeout() { 685 long result = mPasswordExpirationDays * DAYS_TO_MSEC; 686 // Add a small offset to the password expiration. This makes it easier to test 687 // by changing (for example) 1 day to 1 day + 5 minutes. If you set an expiration 688 // that is within the warning period, you should get a warning fairly quickly. 689 if (result > 0) { 690 result += EXPIRATION_OFFSET_MSEC; 691 } 692 return result; 693 } 694 695 /** 696 * Record flags (and a sync key for the flags) into an Account 697 * Note: the hash code is defined as the encoding used in Account 698 * 699 * @param account to write the values mSecurityFlags and mSecuritySyncKey 700 * @param syncKey the value to write into the account's mSecuritySyncKey 701 * @param update if true, also writes the account back to the provider (updating only 702 * the fields changed by this API) 703 * @param context a context for writing to the provider 704 * @return true if the actual policies changed, false if no change (note, sync key 705 * does not affect this) 706 */ 707 public boolean writeAccount(Account account, String syncKey, boolean update, 708 Context context) { 709 long newFlags = getSecurityCode(); 710 boolean dirty = (newFlags != account.mSecurityFlags); 711 account.mSecurityFlags = newFlags; 712 account.mSecuritySyncKey = syncKey; 713 if (update) { 714 if (account.isSaved()) { 715 ContentValues cv = new ContentValues(); 716 cv.put(AccountColumns.SECURITY_FLAGS, account.mSecurityFlags); 717 cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey); 718 account.update(context, cv); 719 } else { 720 account.save(context); 721 } 722 } 723 return dirty; 724 } 725 726 @Override 727 public boolean equals(Object o) { 728 if (o instanceof PolicySet) { 729 PolicySet other = (PolicySet)o; 730 return (this.getSecurityCode() == other.getSecurityCode()); 731 } 732 return false; 733 } 734 735 /** 736 * Supports Parcelable 737 */ 738 public int describeContents() { 739 return 0; 740 } 741 742 /** 743 * Supports Parcelable 744 */ 745 public static final Parcelable.Creator<PolicySet> CREATOR 746 = new Parcelable.Creator<PolicySet>() { 747 public PolicySet createFromParcel(Parcel in) { 748 return new PolicySet(in); 749 } 750 751 public PolicySet[] newArray(int size) { 752 return new PolicySet[size]; 753 } 754 }; 755 756 /** 757 * Supports Parcelable 758 */ 759 public void writeToParcel(Parcel dest, int flags) { 760 dest.writeInt(mMinPasswordLength); 761 dest.writeInt(mPasswordMode); 762 dest.writeInt(mMaxPasswordFails); 763 dest.writeInt(mMaxScreenLockTime); 764 dest.writeInt(mRequireRemoteWipe ? 1 : 0); 765 dest.writeInt(mPasswordExpirationDays); 766 dest.writeInt(mPasswordHistory); 767 dest.writeInt(mPasswordComplexChars); 768 dest.writeInt(mRequireEncryption ? 1 : 0); 769 } 770 771 /** 772 * Supports Parcelable 773 */ 774 public PolicySet(Parcel in) { 775 mMinPasswordLength = in.readInt(); 776 mPasswordMode = in.readInt(); 777 mMaxPasswordFails = in.readInt(); 778 mMaxScreenLockTime = in.readInt(); 779 mRequireRemoteWipe = in.readInt() == 1; 780 mPasswordExpirationDays = in.readInt(); 781 mPasswordHistory = in.readInt(); 782 mPasswordComplexChars = in.readInt(); 783 mRequireEncryption = in.readInt() == 1; 784 } 785 786 @Override 787 public int hashCode() { 788 long code = getSecurityCode(); 789 return (int) code; 790 } 791 792 public long getSecurityCode() { 793 long flags = 0; 794 flags = (long)mMinPasswordLength << PASSWORD_LENGTH_SHIFT; 795 flags |= mPasswordMode; 796 flags |= (long)mMaxPasswordFails << PASSWORD_MAX_FAILS_SHIFT; 797 flags |= (long)mMaxScreenLockTime << SCREEN_LOCK_TIME_SHIFT; 798 if (mRequireRemoteWipe) flags |= REQUIRE_REMOTE_WIPE; 799 flags |= (long)mPasswordHistory << PASSWORD_HISTORY_SHIFT; 800 flags |= (long)mPasswordExpirationDays << PASSWORD_EXPIRATION_SHIFT; 801 flags |= (long)mPasswordComplexChars << PASSWORD_COMPLEX_CHARS_SHIFT; 802 if (mRequireEncryption) flags |= REQUIRE_ENCRYPTION; 803 return flags; 804 } 805 806 @Override 807 public String toString() { 808 return "{ " + "pw-len-min=" + mMinPasswordLength + " pw-mode=" + mPasswordMode 809 + " pw-fails-max=" + mMaxPasswordFails + " screenlock-max=" 810 + mMaxScreenLockTime + " remote-wipe-req=" + mRequireRemoteWipe 811 + " pw-expiration=" + mPasswordExpirationDays 812 + " pw-history=" + mPasswordHistory 813 + " pw-complex-chars=" + mPasswordComplexChars 814 + " require-encryption=" + mRequireEncryption + "}"; 815 } 816 } 817 818 /** 819 * If we are not the active device admin, try to become so. 820 * 821 * Also checks for any policies that we have added during the lifetime of this app. 822 * This catches the case where the user granted an earlier (smaller) set of policies 823 * but an app upgrade requires that new policies be granted. 824 * 825 * @return true if we are already active, false if we are not 826 */ 827 public boolean isActiveAdmin() { 828 DevicePolicyManager dpm = getDPM(); 829 return dpm.isAdminActive(mAdminName) 830 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_POLICY_EXPIRE_PASSWORD) 831 && dpm.hasGrantedPolicy(mAdminName, DeviceAdminInfo.USES_ENCRYPTED_STORAGE); 832 } 833 834 /** 835 * Report admin component name - for making calls into device policy manager 836 */ 837 public ComponentName getAdminComponent() { 838 return mAdminName; 839 } 840 841 /** 842 * Delete all accounts whose security flags aren't zero (i.e. they have security enabled). 843 * This method is synchronous, so it should normally be called within a worker thread (the 844 * exception being for unit tests) 845 * 846 * @param context the caller's context 847 */ 848 /*package*/ void deleteSecuredAccounts(Context context) { 849 ContentResolver cr = context.getContentResolver(); 850 // Find all accounts with security and delete them 851 Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, 852 AccountColumns.SECURITY_FLAGS + "!=0", null, null); 853 try { 854 Log.w(TAG, "Email administration disabled; deleting " + c.getCount() + 855 " secured account(s)"); 856 while (c.moveToNext()) { 857 Controller.getInstance(context).deleteAccountSync( 858 c.getLong(EmailContent.ID_PROJECTION_COLUMN), context); 859 } 860 } finally { 861 c.close(); 862 } 863 updatePolicies(-1); 864 } 865 866 /** 867 * Internal handler for enabled->disabled transitions. Deletes all secured accounts. 868 * Must call from worker thread, not on UI thread. 869 */ 870 /*package*/ void onAdminEnabled(boolean isEnabled) { 871 if (!isEnabled) { 872 deleteSecuredAccounts(mContext); 873 } 874 } 875 876 /** 877 * Handle password expiration - if any accounts appear to have triggered this, put up 878 * warnings, or even shut them down. 879 * 880 * NOTE: If there are multiple accounts with password expiration policies, the device 881 * password will be set to expire in the shortest required interval (most secure). The logic 882 * in this method operates based on the aggregate setting - irrespective of which account caused 883 * the expiration. In other words, all accounts (that require expiration) will run/stop 884 * based on the requirements of the account with the shortest interval. 885 */ 886 private void onPasswordExpiring(Context context) { 887 // 1. Do we have any accounts that matter here? 888 long nextExpiringAccountId = findShortestExpiration(context); 889 890 // 2. If not, exit immediately 891 if (nextExpiringAccountId == -1) { 892 return; 893 } 894 895 // 3. If yes, are we warning or expired? 896 long expirationDate = getDPM().getPasswordExpiration(mAdminName); 897 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 898 boolean expired = timeUntilExpiration < 0; 899 if (!expired) { 900 // 4. If warning, simply put up a generic notification and report that it came from 901 // the shortest-expiring account. 902 Account account = Account.restoreAccountWithId(context, nextExpiringAccountId); 903 if (account == null) return; 904 Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 905 String ticker = context.getString( 906 R.string.password_expire_warning_ticker_fmt, account.getDisplayName()); 907 String contentTitle = context.getString( 908 R.string.password_expire_warning_content_title); 909 String contentText = context.getString( 910 R.string.password_expire_warning_content_text_fmt, account.getDisplayName()); 911 NotificationController nc = NotificationController.getInstance(mContext); 912 nc.postAccountNotification(account, ticker, contentTitle, contentText, intent, 913 NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING); 914 } else { 915 // 5. Actually expired - find all accounts that expire passwords, and wipe them 916 boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context)); 917 if (wiped) { 918 // Post notification 919 Account account = Account.restoreAccountWithId(context, nextExpiringAccountId); 920 if (account == null) return; 921 Intent intent = 922 new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 923 String ticker = context.getString(R.string.password_expired_ticker); 924 String contentTitle = context.getString(R.string.password_expired_content_title); 925 String contentText = context.getString(R.string.password_expired_content_text); 926 NotificationController nc = NotificationController.getInstance(mContext); 927 nc.postAccountNotification(account, ticker, contentTitle, 928 contentText, intent, 929 NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED); 930 } 931 } 932 } 933 934 /** 935 * Find the account with the shortest expiration time. This is always assumed to be 936 * the account that forces the password to be refreshed. 937 * @return -1 if no expirations, or accountId if one is found 938 */ 939 /* package */ static long findShortestExpiration(Context context) { 940 long nextExpiringAccountId = -1; 941 long shortestExpiration = Long.MAX_VALUE; 942 Cursor c = context.getContentResolver().query(Account.CONTENT_URI, 943 ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); 944 try { 945 while (c.moveToNext()) { 946 long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); 947 if (flags != 0) { 948 PolicySet p = new PolicySet(flags); 949 if (p.mPasswordExpirationDays > 0 && 950 p.mPasswordExpirationDays < shortestExpiration) { 951 nextExpiringAccountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID); 952 shortestExpiration = p.mPasswordExpirationDays; 953 } 954 } 955 } 956 } finally { 957 c.close(); 958 } 959 return nextExpiringAccountId; 960 } 961 962 /** 963 * For all accounts that require password expiration, put them in security hold and wipe 964 * their data. 965 * @param context 966 * @param controller 967 * @return true if one or more accounts were wiped 968 */ 969 /* package */ static boolean wipeExpiredAccounts(Context context, Controller controller) { 970 boolean result = false; 971 Cursor c = context.getContentResolver().query(Account.CONTENT_URI, 972 ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); 973 try { 974 while (c.moveToNext()) { 975 long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); 976 if (flags != 0) { 977 PolicySet p = new PolicySet(flags); 978 if (p.mPasswordExpirationDays > 0) { 979 long accountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID); 980 Account account = Account.restoreAccountWithId(context, accountId); 981 if (account != null) { 982 // Mark the account as "on hold". 983 setAccountHoldFlag(context, account, true); 984 // Erase data 985 controller.deleteSyncedDataSync(accountId); 986 // Report one or more were found 987 result = true; 988 } 989 } 990 } 991 } 992 } finally { 993 c.close(); 994 } 995 return result; 996 } 997 998 /** 999 * Callback from EmailBroadcastProcessorService. This provides the workers for the 1000 * DeviceAdminReceiver calls. These should perform the work directly and not use async 1001 * threads for completion. 1002 */ 1003 public static void onDeviceAdminReceiverMessage(Context context, int message) { 1004 SecurityPolicy instance = SecurityPolicy.getInstance(context); 1005 switch (message) { 1006 case DEVICE_ADMIN_MESSAGE_ENABLED: 1007 instance.onAdminEnabled(true); 1008 break; 1009 case DEVICE_ADMIN_MESSAGE_DISABLED: 1010 instance.onAdminEnabled(false); 1011 break; 1012 case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED: 1013 // TODO make a small helper for this 1014 // Clear security holds (if any) 1015 Account.clearSecurityHoldOnAllAccounts(context); 1016 // Cancel any active notifications (if any are posted) 1017 NotificationController nc = NotificationController.getInstance(context); 1018 nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING); 1019 nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED); 1020 break; 1021 case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING: 1022 instance.onPasswordExpiring(instance.mContext); 1023 break; 1024 } 1025 } 1026 1027 /** 1028 * Device Policy administrator. This is primarily a listener for device state changes. 1029 * Note: This is instantiated by incoming messages. 1030 * Note: This is actually a BroadcastReceiver and must remain within the guidelines required 1031 * for proper behavior, including avoidance of ANRs. 1032 * Note: We do not implement onPasswordFailed() because the default behavior of the 1033 * DevicePolicyManager - complete local wipe after 'n' failures - is sufficient. 1034 */ 1035 public static class PolicyAdmin extends DeviceAdminReceiver { 1036 1037 /** 1038 * Called after the administrator is first enabled. 1039 */ 1040 @Override 1041 public void onEnabled(Context context, Intent intent) { 1042 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 1043 DEVICE_ADMIN_MESSAGE_ENABLED); 1044 } 1045 1046 /** 1047 * Called prior to the administrator being disabled. 1048 */ 1049 @Override 1050 public void onDisabled(Context context, Intent intent) { 1051 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 1052 DEVICE_ADMIN_MESSAGE_DISABLED); 1053 } 1054 1055 /** 1056 * Called when the user asks to disable administration; we return a warning string that 1057 * will be presented to the user 1058 */ 1059 @Override 1060 public CharSequence onDisableRequested(Context context, Intent intent) { 1061 return context.getString(R.string.disable_admin_warning); 1062 } 1063 1064 /** 1065 * Called after the user has changed their password. 1066 */ 1067 @Override 1068 public void onPasswordChanged(Context context, Intent intent) { 1069 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 1070 DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED); 1071 } 1072 1073 /** 1074 * Called when device password is expiring 1075 */ 1076 @Override 1077 public void onPasswordExpiring(Context context, Intent intent) { 1078 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 1079 DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING); 1080 } 1081 } 1082} 1083