LockPatternUtils.java revision ba87e3e6c985e7175152993b5efcc7dd2f0e1c93
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.content.ContentResolver; 20import android.os.SystemClock; 21import android.provider.Settings; 22import android.security.MessageDigest; 23import android.text.TextUtils; 24import android.util.Log; 25 26import com.google.android.collect.Lists; 27 28import java.io.FileNotFoundException; 29import java.io.IOException; 30import java.io.RandomAccessFile; 31import java.security.NoSuchAlgorithmException; 32import java.util.Arrays; 33import java.util.List; 34 35/** 36 * Utilities for the lock patten and its settings. 37 */ 38public class LockPatternUtils { 39 40 private static final String TAG = "LockPatternUtils"; 41 42 private static final String LOCK_PATTERN_FILE = "/system/gesture.key"; 43 44 /** 45 * The maximum number of incorrect attempts before the user is prevented 46 * from trying again for {@link #FAILED_ATTEMPT_TIMEOUT_MS}. 47 */ 48 public static final int FAILED_ATTEMPTS_BEFORE_TIMEOUT = 5; 49 50 /** 51 * The number of incorrect attempts before which we fall back on an alternative 52 * method of verifying the user, and resetting their lock pattern. 53 */ 54 public static final int FAILED_ATTEMPTS_BEFORE_RESET = 20; 55 56 /** 57 * How long the user is prevented from trying again after entering the 58 * wrong pattern too many times. 59 */ 60 public static final long FAILED_ATTEMPT_TIMEOUT_MS = 30000L; 61 62 /** 63 * The interval of the countdown for showing progress of the lockout. 64 */ 65 public static final long FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS = 1000L; 66 67 /** 68 * The minimum number of dots in a valid pattern. 69 */ 70 public static final int MIN_LOCK_PATTERN_SIZE = 4; 71 72 /** 73 * The minimum number of dots the user must include in a wrong pattern 74 * attempt for it to be counted against the counts that affect 75 * {@link #FAILED_ATTEMPTS_BEFORE_TIMEOUT} and {@link #FAILED_ATTEMPTS_BEFORE_RESET} 76 */ 77 public static final int MIN_PATTERN_REGISTER_FAIL = 3; 78 79 private final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently"; 80 private final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline"; 81 private final static String PATTERN_EVER_CHOSEN = "lockscreen.patterneverchosen"; 82 83 private final ContentResolver mContentResolver; 84 85 private static String sLockPatternFilename; 86 87 /** 88 * @param contentResolver Used to look up and save settings. 89 */ 90 public LockPatternUtils(ContentResolver contentResolver) { 91 mContentResolver = contentResolver; 92 // Initialize the location of gesture lock file 93 if (sLockPatternFilename == null) { 94 sLockPatternFilename = android.os.Environment.getDataDirectory() 95 .getAbsolutePath() + LOCK_PATTERN_FILE; 96 } 97 } 98 99 /** 100 * Check to see if a pattern matches the saved pattern. If no pattern exists, 101 * always returns true. 102 * @param pattern The pattern to check. 103 * @return Whether the pattern matchees the stored one. 104 */ 105 public boolean checkPattern(List<LockPatternView.Cell> pattern) { 106 try { 107 // Read all the bytes from the file 108 RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "r"); 109 final byte[] stored = new byte[(int) raf.length()]; 110 int got = raf.read(stored, 0, stored.length); 111 raf.close(); 112 if (got <= 0) { 113 return true; 114 } 115 // Compare the hash from the file with the entered pattern's hash 116 return Arrays.equals(stored, LockPatternUtils.patternToHash(pattern)); 117 } catch (FileNotFoundException fnfe) { 118 return true; 119 } catch (IOException ioe) { 120 return true; 121 } 122 } 123 124 /** 125 * Check to see if the user has stored a lock pattern. 126 * @return Whether a saved pattern exists. 127 */ 128 public boolean savedPatternExists() { 129 try { 130 // Check if we can read a byte from the file 131 RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "r"); 132 byte first = raf.readByte(); 133 raf.close(); 134 return true; 135 } catch (FileNotFoundException fnfe) { 136 return false; 137 } catch (IOException ioe) { 138 return false; 139 } 140 } 141 142 /** 143 * Return true if the user has ever chosen a pattern. This is true even if the pattern is 144 * currently cleared. 145 * 146 * @return True if the user has ever chosen a pattern. 147 */ 148 public boolean isPatternEverChosen() { 149 return getBoolean(PATTERN_EVER_CHOSEN); 150 } 151 152 /** 153 * Save a lock pattern. 154 * @param pattern The new pattern to save. 155 */ 156 public void saveLockPattern(List<LockPatternView.Cell> pattern) { 157 // Compute the hash 158 final byte[] hash = LockPatternUtils.patternToHash(pattern); 159 try { 160 // Write the hash to file 161 RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "rw"); 162 // Truncate the file if pattern is null, to clear the lock 163 if (pattern == null) { 164 raf.setLength(0); 165 } else { 166 raf.write(hash, 0, hash.length); 167 } 168 raf.close(); 169 setBoolean(PATTERN_EVER_CHOSEN, true); 170 } catch (FileNotFoundException fnfe) { 171 // Cant do much, unless we want to fail over to using the settings provider 172 Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename); 173 } catch (IOException ioe) { 174 // Cant do much 175 Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename); 176 } 177 } 178 179 /** 180 * Deserialize a pattern. 181 * @param string The pattern serialized with {@link #patternToString} 182 * @return The pattern. 183 */ 184 public static List<LockPatternView.Cell> stringToPattern(String string) { 185 List<LockPatternView.Cell> result = Lists.newArrayList(); 186 187 final byte[] bytes = string.getBytes(); 188 for (int i = 0; i < bytes.length; i++) { 189 byte b = bytes[i]; 190 result.add(LockPatternView.Cell.of(b / 3, b % 3)); 191 } 192 return result; 193 } 194 195 /** 196 * Serialize a pattern. 197 * @param pattern The pattern. 198 * @return The pattern in string form. 199 */ 200 public static String patternToString(List<LockPatternView.Cell> pattern) { 201 if (pattern == null) { 202 return ""; 203 } 204 final int patternSize = pattern.size(); 205 206 byte[] res = new byte[patternSize]; 207 for (int i = 0; i < patternSize; i++) { 208 LockPatternView.Cell cell = pattern.get(i); 209 res[i] = (byte) (cell.getRow() * 3 + cell.getColumn()); 210 } 211 return new String(res); 212 } 213 214 /* 215 * Generate an SHA-1 hash for the pattern. Not the most secure, but it is 216 * at least a second level of protection. First level is that the file 217 * is in a location only readable by the system process. 218 * @param pattern the gesture pattern. 219 * @return the hash of the pattern in a byte array. 220 */ 221 static byte[] patternToHash(List<LockPatternView.Cell> pattern) { 222 if (pattern == null) { 223 return null; 224 } 225 226 final int patternSize = pattern.size(); 227 byte[] res = new byte[patternSize]; 228 for (int i = 0; i < patternSize; i++) { 229 LockPatternView.Cell cell = pattern.get(i); 230 res[i] = (byte) (cell.getRow() * 3 + cell.getColumn()); 231 } 232 try { 233 MessageDigest md = MessageDigest.getInstance("SHA-1"); 234 byte[] hash = md.digest(res); 235 return hash; 236 } catch (NoSuchAlgorithmException nsa) { 237 return res; 238 } 239 } 240 241 /** 242 * @return Whether the lock pattern is enabled. 243 */ 244 public boolean isLockPatternEnabled() { 245 return getBoolean(Settings.System.LOCK_PATTERN_ENABLED); 246 } 247 248 /** 249 * Set whether the lock pattern is enabled. 250 */ 251 public void setLockPatternEnabled(boolean enabled) { 252 setBoolean(Settings.System.LOCK_PATTERN_ENABLED, enabled); 253 } 254 255 /** 256 * @return Whether the visible pattern is enabled. 257 */ 258 public boolean isVisiblePatternEnabled() { 259 return getBoolean(Settings.System.LOCK_PATTERN_VISIBLE); 260 } 261 262 /** 263 * Set whether the visible pattern is enabled. 264 */ 265 public void setVisiblePatternEnabled(boolean enabled) { 266 setBoolean(Settings.System.LOCK_PATTERN_VISIBLE, enabled); 267 } 268 269 /** 270 * @return Whether tactile feedback for the pattern is enabled. 271 */ 272 public boolean isTactileFeedbackEnabled() { 273 return getBoolean(Settings.System.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED); 274 } 275 276 /** 277 * Set whether tactile feedback for the pattern is enabled. 278 */ 279 public void setTactileFeedbackEnabled(boolean enabled) { 280 setBoolean(Settings.System.LOCK_PATTERN_TACTILE_FEEDBACK_ENABLED, enabled); 281 } 282 283 /** 284 * Set and store the lockout deadline, meaning the user can't attempt his/her unlock 285 * pattern until the deadline has passed. 286 * @return the chosen deadline. 287 */ 288 public long setLockoutAttemptDeadline() { 289 final long deadline = SystemClock.elapsedRealtime() + FAILED_ATTEMPT_TIMEOUT_MS; 290 setLong(LOCKOUT_ATTEMPT_DEADLINE, deadline); 291 return deadline; 292 } 293 294 /** 295 * @return The elapsed time in millis in the future when the user is allowed to 296 * attempt to enter his/her lock pattern, or 0 if the user is welcome to 297 * enter a pattern. 298 */ 299 public long getLockoutAttemptDeadline() { 300 final long deadline = getLong(LOCKOUT_ATTEMPT_DEADLINE, 0L); 301 final long now = SystemClock.elapsedRealtime(); 302 if (deadline < now || deadline > (now + FAILED_ATTEMPT_TIMEOUT_MS)) { 303 return 0L; 304 } 305 return deadline; 306 } 307 308 /** 309 * @return Whether the user is permanently locked out until they verify their 310 * credentials. Occurs after {@link #FAILED_ATTEMPTS_BEFORE_RESET} failed 311 * attempts. 312 */ 313 public boolean isPermanentlyLocked() { 314 return getBoolean(LOCKOUT_PERMANENT_KEY); 315 } 316 317 /** 318 * Set the state of whether the device is permanently locked, meaning the user 319 * must authenticate via other means. If false, that means the user has gone 320 * out of permanent lock, so the existing (forgotten) lock pattern needs to 321 * be cleared. 322 * @param locked Whether the user is permanently locked out until they verify their 323 * credentials. Occurs after {@link #FAILED_ATTEMPTS_BEFORE_RESET} failed 324 * attempts. 325 */ 326 public void setPermanentlyLocked(boolean locked) { 327 setBoolean(LOCKOUT_PERMANENT_KEY, locked); 328 329 if (!locked) { 330 setLockPatternEnabled(false); 331 saveLockPattern(null); 332 } 333 } 334 335 /** 336 * @return A formatted string of the next alarm (for showing on the lock screen), 337 * or null if there is no next alarm. 338 */ 339 public String getNextAlarm() { 340 String nextAlarm = Settings.System.getString(mContentResolver, 341 Settings.System.NEXT_ALARM_FORMATTED); 342 if (nextAlarm == null || TextUtils.isEmpty(nextAlarm)) { 343 return null; 344 } 345 return nextAlarm; 346 } 347 348 private boolean getBoolean(String systemSettingKey) { 349 return 1 == 350 android.provider.Settings.System.getInt( 351 mContentResolver, 352 systemSettingKey, 0); 353 } 354 355 private void setBoolean(String systemSettingKey, boolean enabled) { 356 android.provider.Settings.System.putInt( 357 mContentResolver, 358 systemSettingKey, 359 enabled ? 1 : 0); 360 } 361 362 private long getLong(String systemSettingKey, long def) { 363 return android.provider.Settings.System.getLong(mContentResolver, systemSettingKey, def); 364 } 365 366 private void setLong(String systemSettingKey, long value) { 367 android.provider.Settings.System.putLong(mContentResolver, systemSettingKey, value); 368 } 369 370 371} 372