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