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