/** * Set the requested security level based on the aggregate set of requests. If the set is empty, * we release our device administration. If the set is non-empty, we only proceed if we are * already active as an admin. */ public void setActivePolicies() { DevicePolicyManager dpm = getDPM(); // compute aggregate set of policies Policy aggregatePolicy = getAggregatePolicy(); // if empty set, detach from policy manager if (aggregatePolicy == Policy.NO_POLICY) { if (DebugUtils.DEBUG) { LogUtils.d(TAG, "setActivePolicies: none, remove admin"); } dpm.removeActiveAdmin(mAdminName); } else if (isActiveAdmin()) { if (DebugUtils.DEBUG) { LogUtils.d(TAG, "setActivePolicies: " + aggregatePolicy); } // set each policy in the policy manager // password mode & length dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality()); dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength); // screen lock time dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000); // local wipe (failed passwords limit) dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails); // password expiration (days until a password expires). API takes mSec. dpm.setPasswordExpirationTimeout( mAdminName, aggregatePolicy.getDPManagerPasswordExpirationTimeout()); // password history length (number of previous passwords that may not be reused) dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory); // password minimum complex characters. // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM, // setting the quality to complex also defaults min symbols=1 and min numeric=1. // We always / safely clear minSymbols & minNumeric to zero (there is no policy // configuration in which we explicitly require a minimum number of digits or symbols.) dpm.setPasswordMinimumSymbols(mAdminName, 0); dpm.setPasswordMinimumNumeric(mAdminName, 0); dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars); // Device capabilities try { // If we are running in a managed policy, it is a securityException to even // call setCameraDisabled(), if is disabled is false. We have to swallow // the exception here. dpm.setCameraDisabled(mAdminName, aggregatePolicy.mDontAllowCamera); } catch (SecurityException e) { LogUtils.d(TAG, "SecurityException in setCameraDisabled, nothing changed"); } // encryption required dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption); } }
/** * For all accounts that require password expiration, put them in security hold and wipe their * data. * * @param context context * @return true if one or more accounts were wiped */ @VisibleForTesting /*package*/ static boolean wipeExpiredAccounts(Context context) { boolean result = false; Cursor c = context .getContentResolver() .query(Policy.CONTENT_URI, Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null); if (c == null) { return false; } try { while (c.moveToNext()) { long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN); long accountId = Policy.getAccountIdWithPolicyKey(context, policyId); if (accountId < 0) continue; Account account = Account.restoreAccountWithId(context, accountId); if (account != null) { // Mark the account as "on hold". setAccountHoldFlag(context, account, true); // Erase data Uri uri = EmailProvider.uiUri("uiaccountdata", accountId); context.getContentResolver().delete(uri, null, null); // Report one or more were found result = true; } } } finally { c.close(); } return result; }
/** * Set the policy for an account atomically; this also removes any other policy associated with * the account and sets the policy key for the account. If policy is null, the policyKey is set to * 0 and the securitySyncKey to null. Also, update the account object to reflect the current * policyKey and securitySyncKey * * @param context the caller's context * @param account the account whose policy is to be set * @param policy the policy to set, or null if we're clearing the policy * @param securitySyncKey the security sync key for this account (ignored if policy is null) */ public static void setAccountPolicy( Context context, Account account, Policy policy, String securitySyncKey) { ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); // Make sure this is a valid policy set if (policy != null) { policy.normalize(); // Add the new policy (no account will yet reference this) ops.add( ContentProviderOperation.newInsert(Policy.CONTENT_URI) .withValues(policy.toContentValues()) .build()); // Make the policyKey of the account our newly created policy, and set the sync key ops.add( ContentProviderOperation.newUpdate( ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) .withValueBackReference(AccountColumns.POLICY_KEY, 0) .withValue(AccountColumns.SECURITY_SYNC_KEY, securitySyncKey) .build()); } else { ops.add( ContentProviderOperation.newUpdate( ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) .withValue(AccountColumns.SECURITY_SYNC_KEY, null) .withValue(AccountColumns.POLICY_KEY, 0) .build()); } // Delete the previous policy associated with this account, if any if (account.mPolicyKey > 0) { ops.add( ContentProviderOperation.newDelete( ContentUris.withAppendedId(Policy.CONTENT_URI, account.mPolicyKey)) .build()); } try { context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); account.refresh(context); syncAccount(context, account); } catch (RemoteException e) { // This is fatal to a remote process throw new IllegalStateException("Exception setting account policy."); } catch (OperationApplicationException e) { // Can't happen; our provider doesn't throw this exception } }
public EmailSyncParser( final Parser parser, final Context context, final ContentResolver resolver, final Mailbox mailbox, final Account account) throws IOException { super(parser, context, resolver, mailbox, account); mMailboxIdAsString = Long.toString(mMailbox.mId); if (mAccount.mPolicyKey != 0) { mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey); } else { mPolicy = null; } }
/** * Find the account with the shortest expiration time. This is always assumed to be the account * that forces the password to be refreshed. * * @return -1 if no expirations, or accountId if one is found */ @VisibleForTesting /*package*/ static long findShortestExpiration(Context context) { long policyId = Utility.getFirstRowLong( context, Policy.CONTENT_URI, Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC", EmailContent.ID_PROJECTION_COLUMN, -1L); if (policyId < 0) return -1L; return Policy.getAccountIdWithPolicyKey(context, policyId); }
/** * API: Sync service should call this any time a sync fails due to isActive() returning false. * This will kick off the notify-acquire-admin-state process and/or increase the security level. * The caller needs to write the required policies into this account before making this call. * Should not be called from UI thread - uses DB lookups to prepare new notifications * * @param accountId the account for which sync cannot proceed */ public void policiesRequired(long accountId) { Account account = Account.restoreAccountWithId(mContext, accountId); // In case the account has been deleted, just return if (account == null) return; if (account.mPolicyKey == 0) return; Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); if (policy == null) return; if (DebugUtils.DEBUG) { LogUtils.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy); } // Mark the account as "on hold". setAccountHoldFlag(mContext, account, true); // Put up an appropriate notification final NotificationController nc = NotificationControllerCreatorHolder.getInstance(mContext); if (policy.mProtocolPoliciesUnsupported == null) { nc.showSecurityNeededNotification(account); } else { nc.showSecurityUnsupportedNotification(account); } }
public void setAccountPolicy(long accountId, Policy policy, String securityKey, boolean notify) { Account account = Account.restoreAccountWithId(mContext, accountId); // In case the account has been deleted, just return if (account == null) { return; } Policy oldPolicy = null; if (account.mPolicyKey > 0) { oldPolicy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); } // If attachment policies have changed, fix up any affected attachment records if (oldPolicy != null && securityKey != null) { if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) || (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) { Policy.setAttachmentFlagsForNewPolicy(mContext, account, policy); } } boolean policyChanged = (oldPolicy == null) || !oldPolicy.equals(policy); if (!policyChanged && (TextUtilities.stringOrNullEquals(securityKey, account.mSecuritySyncKey))) { LogUtils.d(Logging.LOG_TAG, "setAccountPolicy; policy unchanged"); } else { setAccountPolicy(mContext, account, policy, securityKey); policiesUpdated(); } boolean setHold = false; final NotificationController nc = NotificationControllerCreatorHolder.getInstance(mContext); if (policy.mProtocolPoliciesUnsupported != null) { // We can't support this, reasons in unsupportedRemotePolicies LogUtils.d( Logging.LOG_TAG, "Notify policies for " + account.mDisplayName + " not supported."); setHold = true; if (notify) { nc.showSecurityUnsupportedNotification(account); } // Erase data Uri uri = EmailProvider.uiUri("uiaccountdata", accountId); mContext.getContentResolver().delete(uri, null, null); } else if (isActive(policy)) { if (policyChanged) { LogUtils.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName + " changed."); if (notify) { // Notify that policies changed nc.showSecurityChangedNotification(account); } } else { LogUtils.d(Logging.LOG_TAG, "Policy is active and unchanged; do not notify."); } } else { setHold = true; LogUtils.d( Logging.LOG_TAG, "Notify policies for " + account.mDisplayName + " are not being enforced."); if (notify) { // Put up a notification nc.showSecurityNeededNotification(account); } } // Set/clear the account hold. setAccountHoldFlag(mContext, account, setHold); }
/** * API: Query used to determine if a given policy is "active" (the device is operating at the * required security level). * * <p>This can be used when syncing a specific account, by passing a specific set of policies for * that account. Or, it can be used at any time to compare the device state against the aggregate * set of device policies stored in all accounts. * * <p>This method is for queries only, and does not trigger any change in device state. * * <p>NOTE: If there are multiple accounts with password expiration policies, the device password * will be set to expire in the shortest required interval (most secure). This method will return * 'false' as soon as the password expires - irrespective of which account caused the expiration. * In other words, all accounts (that require expiration) will run/stop based on the requirements * of the account with the shortest interval. * * @param policy the policies requested, or null to check aggregate stored policies * @return zero if the requested policies are active, non-zero bits indicates that more work is * needed (typically, by the user) before the required security polices are fully active. */ public int getInactiveReasons(Policy policy) { // select aggregate set if needed if (policy == null) { policy = getAggregatePolicy(); } // quick check for the "empty set" of no policies if (policy == Policy.NO_POLICY) { return 0; } int reasons = 0; DevicePolicyManager dpm = getDPM(); if (isActiveAdmin()) { // check each policy explicitly if (policy.mPasswordMinLength > 0) { if (dpm.getPasswordMinimumLength(mAdminName) < policy.mPasswordMinLength) { reasons |= INACTIVE_NEED_PASSWORD; } } if (policy.mPasswordMode > 0) { if (dpm.getPasswordQuality(mAdminName) < policy.getDPManagerPasswordQuality()) { reasons |= INACTIVE_NEED_PASSWORD; } if (!dpm.isActivePasswordSufficient()) { reasons |= INACTIVE_NEED_PASSWORD; } } if (policy.mMaxScreenLockTime > 0) { // Note, we use seconds, dpm uses milliseconds if (dpm.getMaximumTimeToLock(mAdminName) > policy.mMaxScreenLockTime * 1000) { reasons |= INACTIVE_NEED_CONFIGURATION; } } if (policy.mPasswordExpirationDays > 0) { // confirm that expirations are currently set long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName); if (currentTimeout == 0 || currentTimeout > policy.getDPManagerPasswordExpirationTimeout()) { reasons |= INACTIVE_NEED_PASSWORD; } // confirm that the current password hasn't expired long expirationDate = dpm.getPasswordExpiration(mAdminName); long timeUntilExpiration = expirationDate - System.currentTimeMillis(); boolean expired = timeUntilExpiration < 0; if (expired) { reasons |= INACTIVE_NEED_PASSWORD; } } if (policy.mPasswordHistory > 0) { if (dpm.getPasswordHistoryLength(mAdminName) < policy.mPasswordHistory) { // There's no user action for changes here; this is just a configuration change reasons |= INACTIVE_NEED_CONFIGURATION; } } if (policy.mPasswordComplexChars > 0) { if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) { reasons |= INACTIVE_NEED_PASSWORD; } } if (policy.mRequireEncryption) { int encryptionStatus = getDPM().getStorageEncryptionStatus(); if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { reasons |= INACTIVE_NEED_ENCRYPTION; } } if (policy.mDontAllowCamera && !dpm.getCameraDisabled(mAdminName)) { reasons |= INACTIVE_NEED_CONFIGURATION; } // password failures are counted locally - no test required here // no check required for remote wipe (it's supported, if we're the admin) if (policy.mProtocolPoliciesUnsupported != null) { reasons |= INACTIVE_PROTOCOL_POLICIES; } // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances. return reasons; } // return false, not active return INACTIVE_NEED_ACTIVATION; }
/** * Compute the aggregate policy for all accounts that require it, and record it. * * <p>The business logic is as follows: min password length take the max password mode take the * max (strongest mode) max password fails take the min max screen lock time take the min require * remote wipe take the max (logical or) password history take the max (strongest mode) password * expiration take the min (strongest mode) password complex chars take the max (strongest mode) * encryption take the max (logical or) * * @return a policy representing the strongest aggregate. If no policy sets are defined, a * lightweight "nothing required" policy will be returned. Never null. */ @VisibleForTesting Policy computeAggregatePolicy() { boolean policiesFound = false; Policy aggregate = new Policy(); aggregate.mPasswordMinLength = Integer.MIN_VALUE; aggregate.mPasswordMode = Integer.MIN_VALUE; aggregate.mPasswordMaxFails = Integer.MAX_VALUE; aggregate.mPasswordHistory = Integer.MIN_VALUE; aggregate.mPasswordExpirationDays = Integer.MAX_VALUE; aggregate.mPasswordComplexChars = Integer.MIN_VALUE; aggregate.mMaxScreenLockTime = Integer.MAX_VALUE; aggregate.mRequireRemoteWipe = false; aggregate.mRequireEncryption = false; // This can never be supported at this time. It exists only for historic reasons where // this was able to be supported prior to the introduction of proper removable storage // support for external storage. aggregate.mRequireEncryptionExternal = false; Cursor c = mContext .getContentResolver() .query(Policy.CONTENT_URI, Policy.CONTENT_PROJECTION, null, null, null); Policy policy = new Policy(); try { while (c.moveToNext()) { policy.restore(c); if (DebugUtils.DEBUG) { LogUtils.d(TAG, "Aggregate from: " + policy); } aggregate.mPasswordMinLength = Math.max(policy.mPasswordMinLength, aggregate.mPasswordMinLength); aggregate.mPasswordMode = Math.max(policy.mPasswordMode, aggregate.mPasswordMode); if (policy.mPasswordMaxFails > 0) { aggregate.mPasswordMaxFails = Math.min(policy.mPasswordMaxFails, aggregate.mPasswordMaxFails); } if (policy.mMaxScreenLockTime > 0) { aggregate.mMaxScreenLockTime = Math.min(policy.mMaxScreenLockTime, aggregate.mMaxScreenLockTime); } if (policy.mPasswordHistory > 0) { aggregate.mPasswordHistory = Math.max(policy.mPasswordHistory, aggregate.mPasswordHistory); } if (policy.mPasswordExpirationDays > 0) { aggregate.mPasswordExpirationDays = Math.min(policy.mPasswordExpirationDays, aggregate.mPasswordExpirationDays); } if (policy.mPasswordComplexChars > 0) { aggregate.mPasswordComplexChars = Math.max(policy.mPasswordComplexChars, aggregate.mPasswordComplexChars); } aggregate.mRequireRemoteWipe |= policy.mRequireRemoteWipe; aggregate.mRequireEncryption |= policy.mRequireEncryption; aggregate.mDontAllowCamera |= policy.mDontAllowCamera; policiesFound = true; } } finally { c.close(); } if (policiesFound) { // final cleanup pass converts any untouched min/max values to zero (not specified) if (aggregate.mPasswordMinLength == Integer.MIN_VALUE) aggregate.mPasswordMinLength = 0; if (aggregate.mPasswordMode == Integer.MIN_VALUE) aggregate.mPasswordMode = 0; if (aggregate.mPasswordMaxFails == Integer.MAX_VALUE) aggregate.mPasswordMaxFails = 0; if (aggregate.mMaxScreenLockTime == Integer.MAX_VALUE) aggregate.mMaxScreenLockTime = 0; if (aggregate.mPasswordHistory == Integer.MIN_VALUE) aggregate.mPasswordHistory = 0; if (aggregate.mPasswordExpirationDays == Integer.MAX_VALUE) aggregate.mPasswordExpirationDays = 0; if (aggregate.mPasswordComplexChars == Integer.MIN_VALUE) aggregate.mPasswordComplexChars = 0; if (DebugUtils.DEBUG) { LogUtils.d(TAG, "Calculated Aggregate: " + aggregate); } return aggregate; } if (DebugUtils.DEBUG) { LogUtils.d(TAG, "Calculated Aggregate: no policy"); } return Policy.NO_POLICY; }