LockPatternUtils.java revision 11b019d07f4de0b25e2f863a7bcaad112d847d56
1/*
2 * Copyright (C) 2007 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.internal.widget;
18
19import android.app.DevicePolicyManager;
20import android.content.ComponentName;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.os.SystemClock;
24import android.provider.Settings;
25import android.security.MessageDigest;
26import android.text.TextUtils;
27import android.util.Log;
28
29import com.google.android.collect.Lists;
30
31import java.io.FileNotFoundException;
32import java.io.IOException;
33import java.io.RandomAccessFile;
34import java.security.NoSuchAlgorithmException;
35import java.security.SecureRandom;
36import java.util.Arrays;
37import java.util.List;
38
39/**
40 * Utilities for the lock patten and its settings.
41 */
42public class LockPatternUtils {
43
44    private static final String TAG = "LockPatternUtils";
45
46    private static final String LOCK_PATTERN_FILE = "/system/gesture.key";
47    private static final String LOCK_PASSWORD_FILE = "/system/password.key";
48
49    /**
50     * The maximum number of incorrect attempts before the user is prevented
51     * from trying again for {@link #FAILED_ATTEMPT_TIMEOUT_MS}.
52     */
53    public static final int FAILED_ATTEMPTS_BEFORE_TIMEOUT = 5;
54
55    /**
56     * The number of incorrect attempts before which we fall back on an alternative
57     * method of verifying the user, and resetting their lock pattern.
58     */
59    public static final int FAILED_ATTEMPTS_BEFORE_RESET = 20;
60
61    /**
62     * How long the user is prevented from trying again after entering the
63     * wrong pattern too many times.
64     */
65    public static final long FAILED_ATTEMPT_TIMEOUT_MS = 30000L;
66
67    /**
68     * The interval of the countdown for showing progress of the lockout.
69     */
70    public static final long FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS = 1000L;
71
72    /**
73     * The minimum number of dots in a valid pattern.
74     */
75    public static final int MIN_LOCK_PATTERN_SIZE = 4;
76
77    /**
78     * Type of password being stored.
79     * pattern = pattern screen
80     * pin = digit-only password
81     * password = alphanumeric password
82     */
83    public static final int MODE_PATTERN = DevicePolicyManager.PASSWORD_MODE_SOMETHING;
84    public static final int MODE_PIN = DevicePolicyManager.PASSWORD_MODE_NUMERIC;
85    public static final int MODE_PASSWORD = DevicePolicyManager.PASSWORD_MODE_ALPHANUMERIC;
86
87    /**
88     * The minimum number of dots the user must include in a wrong pattern
89     * attempt for it to be counted against the counts that affect
90     * {@link #FAILED_ATTEMPTS_BEFORE_TIMEOUT} and {@link #FAILED_ATTEMPTS_BEFORE_RESET}
91     */
92    public static final int MIN_PATTERN_REGISTER_FAIL = 3;
93
94    private final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently";
95    private final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline";
96    private final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen";
97    public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type";
98    private final static String LOCK_PASSWORD_SALT_KEY = "lockscreen.password_salt";
99
100    private final Context mContext;
101    private final ContentResolver mContentResolver;
102    private DevicePolicyManager mDevicePolicyManager;
103    private static String sLockPatternFilename;
104    private static String sLockPasswordFilename;
105
106    /**
107     * @param contentResolver Used to look up and save settings.
108     */
109    public LockPatternUtils(Context context) {
110        mContext = context;
111        mContentResolver = context.getContentResolver();
112        mDevicePolicyManager =
113                (DevicePolicyManager)context.getSystemService(Context.DEVICE_POLICY_SERVICE);
114        // Initialize the location of gesture lock file
115        if (sLockPatternFilename == null) {
116            sLockPatternFilename = android.os.Environment.getDataDirectory()
117                    .getAbsolutePath() + LOCK_PATTERN_FILE;
118            sLockPasswordFilename = android.os.Environment.getDataDirectory()
119                    .getAbsolutePath() + LOCK_PASSWORD_FILE;
120        }
121
122    }
123
124    public boolean isDevicePolicyActive() {
125        ComponentName admin = mDevicePolicyManager.getActiveAdmin();
126        return admin != null ? mDevicePolicyManager.isAdminActive(admin) : false;
127    }
128
129    public int getRequestedMinimumPasswordLength() {
130        return mDevicePolicyManager.getMinimumPasswordLength();
131    }
132
133    /**
134     * Gets the device policy password mode. If the mode is non-specific, returns
135     * MODE_PATTERN which allows the user to choose anything.
136     *
137     * @return
138     */
139    public int getRequestedPasswordMode() {
140        int policyMode = mDevicePolicyManager.getPasswordMode();
141        switch (policyMode) {
142            case DevicePolicyManager.PASSWORD_MODE_ALPHANUMERIC:
143                return MODE_PASSWORD;
144            case DevicePolicyManager.PASSWORD_MODE_NUMERIC:
145                return MODE_PIN;
146            case DevicePolicyManager.PASSWORD_MODE_UNSPECIFIED:
147                return MODE_PATTERN;
148        }
149        return MODE_PATTERN;
150    }
151
152    /**
153     * Returns the actual password mode, as set by keyguard after updating the password.
154     *
155     * @return
156     */
157    public void reportFailedPasswordAttempt() {
158        mDevicePolicyManager.reportFailedPasswordAttempt();
159    }
160
161    public void reportSuccessfulPasswordAttempt() {
162        mDevicePolicyManager.reportSuccessfulPasswordAttempt();
163    }
164
165    public void setActivePasswordState(int mode, int length) {
166        int policyMode = DevicePolicyManager.PASSWORD_MODE_UNSPECIFIED;
167        switch (mode) {
168            case MODE_PATTERN:
169                policyMode = DevicePolicyManager.PASSWORD_MODE_UNSPECIFIED;
170                break;
171            case MODE_PIN:
172                policyMode = DevicePolicyManager.PASSWORD_MODE_NUMERIC;
173                break;
174            case MODE_PASSWORD:
175                policyMode = DevicePolicyManager.PASSWORD_MODE_ALPHANUMERIC;
176                break;
177        }
178        mDevicePolicyManager.setActivePasswordState(policyMode, length);
179    }
180
181    /**
182     * Check to see if a pattern matches the saved pattern.  If no pattern exists,
183     * always returns true.
184     * @param pattern The pattern to check.
185     * @return Whether the pattern matches the stored one.
186     */
187    public boolean checkPattern(List<LockPatternView.Cell> pattern) {
188        try {
189            // Read all the bytes from the file
190            RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "r");
191            final byte[] stored = new byte[(int) raf.length()];
192            int got = raf.read(stored, 0, stored.length);
193            raf.close();
194            if (got <= 0) {
195                return true;
196            }
197            // Compare the hash from the file with the entered pattern's hash
198            return Arrays.equals(stored, LockPatternUtils.patternToHash(pattern));
199        } catch (FileNotFoundException fnfe) {
200            return true;
201        } catch (IOException ioe) {
202            return true;
203        }
204    }
205
206    /**
207     * Check to see if a password matches the saved password.  If no password exists,
208     * always returns true.
209     * @param password The password to check.
210     * @return Whether the password matches the stored one.
211     */
212    public boolean checkPassword(String password) {
213        try {
214            // Read all the bytes from the file
215            RandomAccessFile raf = new RandomAccessFile(sLockPasswordFilename, "r");
216            final byte[] stored = new byte[(int) raf.length()];
217            int got = raf.read(stored, 0, stored.length);
218            raf.close();
219            if (got <= 0) {
220                return true;
221            }
222            // Compare the hash from the file with the entered password's hash
223            return Arrays.equals(stored, passwordToHash(password));
224        } catch (FileNotFoundException fnfe) {
225            return true;
226        } catch (IOException ioe) {
227            return true;
228        }
229    }
230
231    /**
232     * Checks to see if the given file exists and contains any data. Returns true if it does,
233     * false otherwise.
234     * @param filename
235     * @return true if file exists and is non-empty.
236     */
237    private boolean nonEmptyFileExists(String filename) {
238        try {
239            // Check if we can read a byte from the file
240            RandomAccessFile raf = new RandomAccessFile(filename, "r");
241            byte first = raf.readByte();
242            raf.close();
243            return true;
244        } catch (FileNotFoundException fnfe) {
245            return false;
246        } catch (IOException ioe) {
247            return false;
248        }
249    }
250
251    /**
252     * Check to see if the user has stored a lock pattern.
253     * @return Whether a saved pattern exists.
254     */
255    public boolean savedPatternExists() {
256        return nonEmptyFileExists(sLockPatternFilename);
257    }
258
259    /**
260     * Check to see if the user has stored a lock pattern.
261     * @return Whether a saved pattern exists.
262     */
263    public boolean savedPasswordExists() {
264        return nonEmptyFileExists(sLockPasswordFilename);
265    }
266
267    /**
268     * Return true if the user has ever chosen a pattern.  This is true even if the pattern is
269     * currently cleared.
270     *
271     * @return True if the user has ever chosen a pattern.
272     */
273    public boolean isPatternEverChosen() {
274        return getBoolean(PATTERN_EVER_CHOSEN_KEY);
275    }
276
277    /**
278     * Clear any lock pattern or password.
279     */
280    public void clearLock() {
281        saveLockPassword(null, LockPatternUtils.MODE_PATTERN);
282        setLockPatternEnabled(false);
283        saveLockPattern(null);
284        setLong(PASSWORD_TYPE_KEY, MODE_PATTERN);
285    }
286
287    /**
288     * Save a lock pattern.
289     * @param pattern The new pattern to save.
290     */
291    public void saveLockPattern(List<LockPatternView.Cell> pattern) {
292        // Compute the hash
293        final byte[] hash  = LockPatternUtils.patternToHash(pattern);
294        try {
295            // Write the hash to file
296            RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "rw");
297            // Truncate the file if pattern is null, to clear the lock
298            if (pattern == null) {
299                raf.setLength(0);
300            } else {
301                raf.write(hash, 0, hash.length);
302            }
303            raf.close();
304            if (pattern != null) {
305                setBoolean(PATTERN_EVER_CHOSEN_KEY, true);
306                setLong(PASSWORD_TYPE_KEY, MODE_PATTERN);
307                DevicePolicyManager dpm = (DevicePolicyManager)mContext.getSystemService(
308                        Context.DEVICE_POLICY_SERVICE);
309                dpm.setActivePasswordState(
310                        DevicePolicyManager.PASSWORD_MODE_SOMETHING, pattern.size());
311            }
312        } catch (FileNotFoundException fnfe) {
313            // Cant do much, unless we want to fail over to using the settings provider
314            Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename);
315        } catch (IOException ioe) {
316            // Cant do much
317            Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename);
318        }
319    }
320
321    /**
322     * Save a lock password.
323     * @param password The password to save
324     */
325    public void saveLockPassword(String password, int mode) {
326        // Compute the hash
327        final byte[] hash = passwordToHash(password);
328        try {
329            // Write the hash to file
330            RandomAccessFile raf = new RandomAccessFile(sLockPasswordFilename, "rw");
331            // Truncate the file if pattern is null, to clear the lock
332            if (password == null) {
333                raf.setLength(0);
334            } else {
335                raf.write(hash, 0, hash.length);
336            }
337            raf.close();
338            if (password != null) {
339                int textMode = TextUtils.isDigitsOnly(password) ? MODE_PIN : MODE_PASSWORD;
340                if (textMode > mode) {
341                    mode = textMode;
342                }
343                setLong(PASSWORD_TYPE_KEY, mode);
344                DevicePolicyManager dpm = (DevicePolicyManager)mContext.getSystemService(
345                        Context.DEVICE_POLICY_SERVICE);
346                dpm.setActivePasswordState(mode, password.length());
347            }
348        } catch (FileNotFoundException fnfe) {
349            // Cant do much, unless we want to fail over to using the settings provider
350            Log.e(TAG, "Unable to save lock pattern to " + sLockPasswordFilename);
351        } catch (IOException ioe) {
352            // Cant do much
353            Log.e(TAG, "Unable to save lock pattern to " + sLockPasswordFilename);
354        }
355    }
356
357    public int getPasswordMode() {
358        return (int) getLong(PASSWORD_TYPE_KEY, MODE_PATTERN);
359    }
360
361    /**
362     * Deserialize a pattern.
363     * @param string The pattern serialized with {@link #patternToString}
364     * @return The pattern.
365     */
366    public static List<LockPatternView.Cell> stringToPattern(String string) {
367        List<LockPatternView.Cell> result = Lists.newArrayList();
368
369        final byte[] bytes = string.getBytes();
370        for (int i = 0; i < bytes.length; i++) {
371            byte b = bytes[i];
372            result.add(LockPatternView.Cell.of(b / 3, b % 3));
373        }
374        return result;
375    }
376
377    /**
378     * Serialize a pattern.
379     * @param pattern The pattern.
380     * @return The pattern in string form.
381     */
382    public static String patternToString(List<LockPatternView.Cell> pattern) {
383        if (pattern == null) {
384            return "";
385        }
386        final int patternSize = pattern.size();
387
388        byte[] res = new byte[patternSize];
389        for (int i = 0; i < patternSize; i++) {
390            LockPatternView.Cell cell = pattern.get(i);
391            res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());
392        }
393        return new String(res);
394    }
395
396    /*
397     * Generate an SHA-1 hash for the pattern. Not the most secure, but it is
398     * at least a second level of protection. First level is that the file
399     * is in a location only readable by the system process.
400     * @param pattern the gesture pattern.
401     * @return the hash of the pattern in a byte array.
402     */
403    private static byte[] patternToHash(List<LockPatternView.Cell> pattern) {
404        if (pattern == null) {
405            return null;
406        }
407
408        final int patternSize = pattern.size();
409        byte[] res = new byte[patternSize];
410        for (int i = 0; i < patternSize; i++) {
411            LockPatternView.Cell cell = pattern.get(i);
412            res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());
413        }
414        try {
415            MessageDigest md = MessageDigest.getInstance("SHA-1");
416            byte[] hash = md.digest(res);
417            return hash;
418        } catch (NoSuchAlgorithmException nsa) {
419            return res;
420        }
421    }
422
423    private String getSalt() {
424        long salt = getLong(LOCK_PASSWORD_SALT_KEY, 0);
425        if (salt == 0) {
426            try {
427                salt = SecureRandom.getInstance("SHA1PRNG").nextLong();
428                setLong(LOCK_PASSWORD_SALT_KEY, salt);
429                Log.v(TAG, "Initialized lock password salt");
430            } catch (NoSuchAlgorithmException e) {
431                // Throw an exception rather than storing a password we'll never be able to recover
432                throw new IllegalStateException("Couldn't get SecureRandom number", e);
433            }
434        }
435        return Long.toHexString(salt);
436    }
437
438    /*
439     * Generate a hash for the given password. To avoid brute force attacks, we use a salted hash.
440     * Not the most secure, but it is at least a second level of protection. First level is that
441     * the file is in a location only readable by the system process.
442     * @param password the gesture pattern.
443     * @return the hash of the pattern in a byte array.
444     */
445     public byte[] passwordToHash(String password) {
446        if (password == null) {
447            return null;
448        }
449        String algo = null;
450        byte[] hashed = null;
451        try {
452            byte[] saltedPassword = (password + getSalt()).getBytes();
453            byte[] sha1 = MessageDigest.getInstance(algo = "SHA-1").digest(saltedPassword);
454            byte[] md5 = MessageDigest.getInstance(algo = "MD5").digest(saltedPassword);
455            hashed = (toHex(sha1) + toHex(md5)).getBytes();
456        } catch (NoSuchAlgorithmException e) {
457            Log.w(TAG, "Failed to encode string because of missing algorithm: " + algo);
458        }
459        return hashed;
460    }
461
462    private static String toHex(byte[] ary) {
463        final String hex = "0123456789ABCDEF";
464        String ret = "";
465        for (int i = 0; i < ary.length; i++) {
466            ret += hex.charAt((ary[i] >> 4) & 0xf);
467            ret += hex.charAt(ary[i] & 0xf);
468        }
469        return ret;
470    }
471
472    /**
473     * @return Whether the lock password is enabled.
474     */
475    public boolean isLockPasswordEnabled() {
476        long mode = getLong(PASSWORD_TYPE_KEY, 0);
477        return savedPasswordExists() && (mode == MODE_PASSWORD || mode == MODE_PIN);
478    }
479
480    /**
481     * @return Whether the lock pattern is enabled.
482     */
483    public boolean isLockPatternEnabled() {
484        return getBoolean(Settings.System.LOCK_PATTERN_ENABLED)
485                && getLong(PASSWORD_TYPE_KEY, MODE_PATTERN) == MODE_PATTERN;
486    }
487
488    /**
489     * Set whether the lock pattern is enabled.
490     */
491    public void setLockPatternEnabled(boolean enabled) {
492        setBoolean(Settings.System.LOCK_PATTERN_ENABLED, enabled);
493    }
494
495    /**
496     * @return Whether the visible pattern is enabled.
497     */
498    public boolean isVisiblePatternEnabled() {
499        return getBoolean(Settings.System.LOCK_PATTERN_VISIBLE);
500    }
501
502    /**
503     * Set whether the visible pattern is enabled.
504     */
505    public void setVisiblePatternEnabled(boolean enabled) {
506        setBoolean(Settings.System.LOCK_PATTERN_VISIBLE, enabled);
507    }
508
509    /**
510     * @return Whether tactile feedback for the pattern is enabled.
511     */
512    public boolean isTactileFeedbackEnabled() {
513        return getBoolean(Settings.System.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED);
514    }
515
516    /**
517     * Set whether tactile feedback for the pattern is enabled.
518     */
519    public void setTactileFeedbackEnabled(boolean enabled) {
520        setBoolean(Settings.System.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED, enabled);
521    }
522
523    /**
524     * Set and store the lockout deadline, meaning the user can't attempt his/her unlock
525     * pattern until the deadline has passed.
526     * @return the chosen deadline.
527     */
528    public long setLockoutAttemptDeadline() {
529        final long deadline = SystemClock.elapsedRealtime() + FAILED_ATTEMPT_TIMEOUT_MS;
530        setLong(LOCKOUT_ATTEMPT_DEADLINE, deadline);
531        return deadline;
532    }
533
534    /**
535     * @return The elapsed time in millis in the future when the user is allowed to
536     *   attempt to enter his/her lock pattern, or 0 if the user is welcome to
537     *   enter a pattern.
538     */
539    public long getLockoutAttemptDeadline() {
540        final long deadline = getLong(LOCKOUT_ATTEMPT_DEADLINE, 0L);
541        final long now = SystemClock.elapsedRealtime();
542        if (deadline < now || deadline > (now + FAILED_ATTEMPT_TIMEOUT_MS)) {
543            return 0L;
544        }
545        return deadline;
546    }
547
548    /**
549     * @return Whether the user is permanently locked out until they verify their
550     *   credentials.  Occurs after {@link #FAILED_ATTEMPTS_BEFORE_RESET} failed
551     *   attempts.
552     */
553    public boolean isPermanentlyLocked() {
554        return getBoolean(LOCKOUT_PERMANENT_KEY);
555    }
556
557    /**
558     * Set the state of whether the device is permanently locked, meaning the user
559     * must authenticate via other means.
560     *
561     * @param locked Whether the user is permanently locked out until they verify their
562     *   credentials.  Occurs after {@link #FAILED_ATTEMPTS_BEFORE_RESET} failed
563     *   attempts.
564     */
565    public void setPermanentlyLocked(boolean locked) {
566        setBoolean(LOCKOUT_PERMANENT_KEY, locked);
567    }
568
569    /**
570     * @return A formatted string of the next alarm (for showing on the lock screen),
571     *   or null if there is no next alarm.
572     */
573    public String getNextAlarm() {
574        String nextAlarm = Settings.System.getString(mContentResolver,
575                Settings.System.NEXT_ALARM_FORMATTED);
576        if (nextAlarm == null || TextUtils.isEmpty(nextAlarm)) {
577            return null;
578        }
579        return nextAlarm;
580    }
581
582    private boolean getBoolean(String systemSettingKey) {
583        // STOPSHIP: these need to be moved to secure settings!
584        return 1 ==
585                android.provider.Settings.System.getInt(
586                        mContentResolver,
587                        systemSettingKey, 0);
588    }
589
590    private void setBoolean(String systemSettingKey, boolean enabled) {
591        // STOPSHIP: these need to be moved to secure settings!
592        android.provider.Settings.System.putInt(
593                        mContentResolver,
594                        systemSettingKey,
595                        enabled ? 1 : 0);
596    }
597
598    private long getLong(String systemSettingKey, long def) {
599        // STOPSHIP: these need to be moved to secure settings!
600        return android.provider.Settings.System.getLong(mContentResolver, systemSettingKey, def);
601    }
602
603    private void setLong(String systemSettingKey, long value) {
604        // STOPSHIP: these need to be moved to secure settings!
605        android.provider.Settings.System.putLong(mContentResolver, systemSettingKey, value);
606    }
607
608    public boolean isSecure() {
609        long mode = getPasswordMode();
610        boolean secure = mode == MODE_PATTERN && isLockPatternEnabled() && savedPatternExists()
611            || (mode == MODE_PIN || mode == MODE_PASSWORD) && savedPasswordExists();
612        return secure;
613    }
614}
615