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