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