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