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