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