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