SecurityPolicy.java revision 469f2987dc11d153434e50eb04dd6b83b924d09d
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().getStorageEncryption(null); 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().getStorageEncryption(null); 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 } 802 803 /** 804 * Report admin component name - for making calls into device policy manager 805 */ 806 public ComponentName getAdminComponent() { 807 return mAdminName; 808 } 809 810 /** 811 * Delete all accounts whose security flags aren't zero (i.e. they have security enabled). 812 * This method is synchronous, so it should normally be called within a worker thread (the 813 * exception being for unit tests) 814 * 815 * @param context the caller's context 816 */ 817 /*package*/ void deleteSecuredAccounts(Context context) { 818 ContentResolver cr = context.getContentResolver(); 819 // Find all accounts with security and delete them 820 Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, 821 AccountColumns.SECURITY_FLAGS + "!=0", null, null); 822 try { 823 Log.w(TAG, "Email administration disabled; deleting " + c.getCount() + 824 " secured account(s)"); 825 while (c.moveToNext()) { 826 Controller.getInstance(context).deleteAccountSync( 827 c.getLong(EmailContent.ID_PROJECTION_COLUMN), context); 828 } 829 } finally { 830 c.close(); 831 } 832 updatePolicies(-1); 833 } 834 835 /** 836 * Internal handler for enabled->disabled transitions. Deletes all secured accounts. 837 * Must call from worker thread, not on UI thread. 838 */ 839 /*package*/ void onAdminEnabled(boolean isEnabled) { 840 if (!isEnabled) { 841 deleteSecuredAccounts(mContext); 842 } 843 } 844 845 /** 846 * Handle password expiration - if any accounts appear to have triggered this, put up 847 * warnings, or even shut them down. 848 * 849 * NOTE: If there are multiple accounts with password expiration policies, the device 850 * password will be set to expire in the shortest required interval (most secure). The logic 851 * in this method operates based on the aggregate setting - irrespective of which account caused 852 * the expiration. In other words, all accounts (that require expiration) will run/stop 853 * based on the requirements of the account with the shortest interval. 854 */ 855 private void onPasswordExpiring(Context context) { 856 // 1. Do we have any accounts that matter here? 857 long nextExpiringAccountId = findShortestExpiration(context); 858 859 // 2. If not, exit immediately 860 if (nextExpiringAccountId == -1) { 861 return; 862 } 863 864 // 3. If yes, are we warning or expired? 865 long expirationDate = getDPM().getPasswordExpiration(mAdminName); 866 long timeUntilExpiration = expirationDate - System.currentTimeMillis(); 867 boolean expired = timeUntilExpiration < 0; 868 if (!expired) { 869 // 4. If warning, simply put up a generic notification and report that it came from 870 // the shortest-expiring account. 871 Account account = Account.restoreAccountWithId(context, nextExpiringAccountId); 872 if (account == null) return; 873 Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 874 String ticker = context.getString( 875 R.string.password_expire_warning_ticker_fmt, account.getDisplayName()); 876 String contentTitle = context.getString( 877 R.string.password_expire_warning_content_title); 878 String contentText = context.getString( 879 R.string.password_expire_warning_content_text_fmt, account.getDisplayName()); 880 NotificationController nc = NotificationController.getInstance(mContext); 881 nc.postAccountNotification(account, ticker, contentTitle, contentText, intent, 882 NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING); 883 } else { 884 // 5. Actually expired - find all accounts that expire passwords, and wipe them 885 boolean wiped = wipeExpiredAccounts(context, Controller.getInstance(context)); 886 if (wiped) { 887 // Post notification 888 Account account = Account.restoreAccountWithId(context, nextExpiringAccountId); 889 if (account == null) return; 890 Intent intent = 891 new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 892 String ticker = context.getString(R.string.password_expired_ticker); 893 String contentTitle = context.getString(R.string.password_expired_content_title); 894 String contentText = context.getString(R.string.password_expired_content_text); 895 NotificationController nc = NotificationController.getInstance(mContext); 896 nc.postAccountNotification(account, ticker, contentTitle, 897 contentText, intent, 898 NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED); 899 } 900 } 901 } 902 903 /** 904 * Find the account with the shortest expiration time. This is always assumed to be 905 * the account that forces the password to be refreshed. 906 * @return -1 if no expirations, or accountId if one is found 907 */ 908 /* package */ static long findShortestExpiration(Context context) { 909 long nextExpiringAccountId = -1; 910 long shortestExpiration = Long.MAX_VALUE; 911 Cursor c = context.getContentResolver().query(Account.CONTENT_URI, 912 ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); 913 try { 914 while (c.moveToNext()) { 915 long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); 916 if (flags != 0) { 917 PolicySet p = new PolicySet(flags); 918 if (p.mPasswordExpirationDays > 0 && 919 p.mPasswordExpirationDays < shortestExpiration) { 920 nextExpiringAccountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID); 921 shortestExpiration = p.mPasswordExpirationDays; 922 } 923 } 924 } 925 } finally { 926 c.close(); 927 } 928 return nextExpiringAccountId; 929 } 930 931 /** 932 * For all accounts that require password expiration, put them in security hold and wipe 933 * their data. 934 * @param context 935 * @param controller 936 * @return true if one or more accounts were wiped 937 */ 938 /* package */ static boolean wipeExpiredAccounts(Context context, Controller controller) { 939 boolean result = false; 940 Cursor c = context.getContentResolver().query(Account.CONTENT_URI, 941 ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); 942 try { 943 while (c.moveToNext()) { 944 long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); 945 if (flags != 0) { 946 PolicySet p = new PolicySet(flags); 947 if (p.mPasswordExpirationDays > 0) { 948 long accountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID); 949 Account account = Account.restoreAccountWithId(context, accountId); 950 if (account != null) { 951 // Mark the account as "on hold". 952 setAccountHoldFlag(context, account, true); 953 // Erase data 954 controller.deleteSyncedDataSync(accountId); 955 // Report one or more were found 956 result = true; 957 } 958 } 959 } 960 } 961 } finally { 962 c.close(); 963 } 964 return result; 965 } 966 967 /** 968 * Callback from EmailBroadcastProcessorService. This provides the workers for the 969 * DeviceAdminReceiver calls. These should perform the work directly and not use async 970 * threads for completion. 971 */ 972 public static void onDeviceAdminReceiverMessage(Context context, int message) { 973 SecurityPolicy instance = SecurityPolicy.getInstance(context); 974 switch (message) { 975 case DEVICE_ADMIN_MESSAGE_ENABLED: 976 instance.onAdminEnabled(true); 977 break; 978 case DEVICE_ADMIN_MESSAGE_DISABLED: 979 instance.onAdminEnabled(false); 980 break; 981 case DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED: 982 // TODO make a small helper for this 983 // Clear security holds (if any) 984 Account.clearSecurityHoldOnAllAccounts(context); 985 // Cancel any active notifications (if any are posted) 986 NotificationController nc = NotificationController.getInstance(context); 987 nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRING); 988 nc.cancelNotification(NotificationController.NOTIFICATION_ID_PASSWORD_EXPIRED); 989 break; 990 case DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING: 991 instance.onPasswordExpiring(instance.mContext); 992 break; 993 } 994 } 995 996 /** 997 * Device Policy administrator. This is primarily a listener for device state changes. 998 * Note: This is instantiated by incoming messages. 999 * Note: This is actually a BroadcastReceiver and must remain within the guidelines required 1000 * for proper behavior, including avoidance of ANRs. 1001 * Note: We do not implement onPasswordFailed() because the default behavior of the 1002 * DevicePolicyManager - complete local wipe after 'n' failures - is sufficient. 1003 */ 1004 public static class PolicyAdmin extends DeviceAdminReceiver { 1005 1006 /** 1007 * Called after the administrator is first enabled. 1008 */ 1009 @Override 1010 public void onEnabled(Context context, Intent intent) { 1011 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 1012 DEVICE_ADMIN_MESSAGE_ENABLED); 1013 } 1014 1015 /** 1016 * Called prior to the administrator being disabled. 1017 */ 1018 @Override 1019 public void onDisabled(Context context, Intent intent) { 1020 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 1021 DEVICE_ADMIN_MESSAGE_DISABLED); 1022 } 1023 1024 /** 1025 * Called when the user asks to disable administration; we return a warning string that 1026 * will be presented to the user 1027 */ 1028 @Override 1029 public CharSequence onDisableRequested(Context context, Intent intent) { 1030 return context.getString(R.string.disable_admin_warning); 1031 } 1032 1033 /** 1034 * Called after the user has changed their password. 1035 */ 1036 @Override 1037 public void onPasswordChanged(Context context, Intent intent) { 1038 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 1039 DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED); 1040 } 1041 1042 /** 1043 * Called when device password is expiring 1044 */ 1045 @Override 1046 public void onPasswordExpiring(Context context, Intent intent) { 1047 EmailBroadcastProcessorService.processDevicePolicyMessage(context, 1048 DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING); 1049 } 1050 } 1051} 1052