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