1 /* 2 * Copyright (C) 2014 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 17 package com.android.server; 18 19 import com.android.internal.annotations.VisibleForTesting; 20 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.pm.UserInfo; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.database.sqlite.SQLiteOpenHelper; 27 import android.os.Environment; 28 import android.os.UserManager; 29 import android.util.ArrayMap; 30 import android.util.Log; 31 import android.util.Slog; 32 import android.util.SparseArray; 33 34 import java.io.File; 35 import java.io.IOException; 36 import java.io.RandomAccessFile; 37 38 import static android.content.Context.USER_SERVICE; 39 40 /** 41 * Storage for the lock settings service. 42 */ 43 class LockSettingsStorage { 44 45 private static final String TAG = "LockSettingsStorage"; 46 private static final String TABLE = "locksettings"; 47 private static final boolean DEBUG = false; 48 49 private static final String COLUMN_KEY = "name"; 50 private static final String COLUMN_USERID = "user"; 51 private static final String COLUMN_VALUE = "value"; 52 53 private static final String[] COLUMNS_FOR_QUERY = { 54 COLUMN_VALUE 55 }; 56 private static final String[] COLUMNS_FOR_PREFETCH = { 57 COLUMN_KEY, COLUMN_VALUE 58 }; 59 60 private static final String SYSTEM_DIRECTORY = "/system/"; 61 private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key"; 62 private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key"; 63 private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key"; 64 private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key"; 65 private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key"; 66 private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key"; 67 68 private static final Object DEFAULT = new Object(); 69 70 private final DatabaseHelper mOpenHelper; 71 private final Context mContext; 72 private final Cache mCache = new Cache(); 73 private final Object mFileWriteLock = new Object(); 74 75 private SparseArray<Integer> mStoredCredentialType; 76 77 static class CredentialHash { 78 static final int TYPE_NONE = -1; 79 static final int TYPE_PATTERN = 1; 80 static final int TYPE_PASSWORD = 2; 81 82 static final int VERSION_LEGACY = 0; 83 static final int VERSION_GATEKEEPER = 1; 84 85 CredentialHash(byte[] hash, int version) { 86 this.hash = hash; 87 this.version = version; 88 this.isBaseZeroPattern = false; 89 } 90 91 CredentialHash(byte[] hash, boolean isBaseZeroPattern) { 92 this.hash = hash; 93 this.version = VERSION_GATEKEEPER; 94 this.isBaseZeroPattern = isBaseZeroPattern; 95 } 96 97 byte[] hash; 98 int version; 99 boolean isBaseZeroPattern; 100 } 101 102 public LockSettingsStorage(Context context, Callback callback) { 103 mContext = context; 104 mOpenHelper = new DatabaseHelper(context, callback); 105 mStoredCredentialType = new SparseArray<Integer>(); 106 } 107 108 public void writeKeyValue(String key, String value, int userId) { 109 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId); 110 } 111 112 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) { 113 ContentValues cv = new ContentValues(); 114 cv.put(COLUMN_KEY, key); 115 cv.put(COLUMN_USERID, userId); 116 cv.put(COLUMN_VALUE, value); 117 118 db.beginTransaction(); 119 try { 120 db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?", 121 new String[] {key, Integer.toString(userId)}); 122 db.insert(TABLE, null, cv); 123 db.setTransactionSuccessful(); 124 mCache.putKeyValue(key, value, userId); 125 } finally { 126 db.endTransaction(); 127 } 128 129 } 130 131 public String readKeyValue(String key, String defaultValue, int userId) { 132 int version; 133 synchronized (mCache) { 134 if (mCache.hasKeyValue(key, userId)) { 135 return mCache.peekKeyValue(key, defaultValue, userId); 136 } 137 version = mCache.getVersion(); 138 } 139 140 Cursor cursor; 141 Object result = DEFAULT; 142 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 143 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY, 144 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?", 145 new String[] { Integer.toString(userId), key }, 146 null, null, null)) != null) { 147 if (cursor.moveToFirst()) { 148 result = cursor.getString(0); 149 } 150 cursor.close(); 151 } 152 mCache.putKeyValueIfUnchanged(key, result, userId, version); 153 return result == DEFAULT ? defaultValue : (String) result; 154 } 155 156 public void prefetchUser(int userId) { 157 int version; 158 synchronized (mCache) { 159 if (mCache.isFetched(userId)) { 160 return; 161 } 162 mCache.setFetched(userId); 163 version = mCache.getVersion(); 164 } 165 166 Cursor cursor; 167 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 168 if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH, 169 COLUMN_USERID + "=?", 170 new String[] { Integer.toString(userId) }, 171 null, null, null)) != null) { 172 while (cursor.moveToNext()) { 173 String key = cursor.getString(0); 174 String value = cursor.getString(1); 175 mCache.putKeyValueIfUnchanged(key, value, userId, version); 176 } 177 cursor.close(); 178 } 179 180 // Populate cache by reading the password and pattern files. 181 readPasswordHash(userId); 182 readPatternHash(userId); 183 } 184 185 public int getStoredCredentialType(int userId) { 186 final Integer cachedStoredCredentialType = mStoredCredentialType.get(userId); 187 if (cachedStoredCredentialType != null) { 188 return cachedStoredCredentialType.intValue(); 189 } 190 191 int storedCredentialType; 192 CredentialHash pattern = readPatternHash(userId); 193 if (pattern == null) { 194 if (readPasswordHash(userId) != null) { 195 storedCredentialType = CredentialHash.TYPE_PASSWORD; 196 } else { 197 storedCredentialType = CredentialHash.TYPE_NONE; 198 } 199 } else { 200 CredentialHash password = readPasswordHash(userId); 201 if (password != null) { 202 // Both will never be GateKeeper 203 if (password.version == CredentialHash.VERSION_GATEKEEPER) { 204 storedCredentialType = CredentialHash.TYPE_PASSWORD; 205 } else { 206 storedCredentialType = CredentialHash.TYPE_PATTERN; 207 } 208 } else { 209 storedCredentialType = CredentialHash.TYPE_PATTERN; 210 } 211 } 212 mStoredCredentialType.put(userId, storedCredentialType); 213 return storedCredentialType; 214 } 215 216 217 public CredentialHash readPasswordHash(int userId) { 218 byte[] stored = readFile(getLockPasswordFilename(userId)); 219 if (stored != null && stored.length > 0) { 220 return new CredentialHash(stored, CredentialHash.VERSION_GATEKEEPER); 221 } 222 223 stored = readFile(getLegacyLockPasswordFilename(userId)); 224 if (stored != null && stored.length > 0) { 225 return new CredentialHash(stored, CredentialHash.VERSION_LEGACY); 226 } 227 228 return null; 229 } 230 231 public CredentialHash readPatternHash(int userId) { 232 byte[] stored = readFile(getLockPatternFilename(userId)); 233 if (stored != null && stored.length > 0) { 234 return new CredentialHash(stored, CredentialHash.VERSION_GATEKEEPER); 235 } 236 237 stored = readFile(getBaseZeroLockPatternFilename(userId)); 238 if (stored != null && stored.length > 0) { 239 return new CredentialHash(stored, true); 240 } 241 242 stored = readFile(getLegacyLockPatternFilename(userId)); 243 if (stored != null && stored.length > 0) { 244 return new CredentialHash(stored, CredentialHash.VERSION_LEGACY); 245 } 246 247 return null; 248 } 249 250 public void removeChildProfileLock(int userId) { 251 if (DEBUG) 252 Slog.e(TAG, "Remove child profile lock for user: " + userId); 253 try { 254 deleteFile(getChildProfileLockFile(userId)); 255 } catch (Exception e) { 256 e.printStackTrace(); 257 } 258 } 259 260 public void writeChildProfileLock(int userId, byte[] lock) { 261 writeFile(getChildProfileLockFile(userId), lock); 262 } 263 264 public byte[] readChildProfileLock(int userId) { 265 return readFile(getChildProfileLockFile(userId)); 266 } 267 268 public boolean hasChildProfileLock(int userId) { 269 return hasFile(getChildProfileLockFile(userId)); 270 } 271 272 public boolean hasPassword(int userId) { 273 return hasFile(getLockPasswordFilename(userId)) || 274 hasFile(getLegacyLockPasswordFilename(userId)); 275 } 276 277 public boolean hasPattern(int userId) { 278 return hasFile(getLockPatternFilename(userId)) || 279 hasFile(getBaseZeroLockPatternFilename(userId)) || 280 hasFile(getLegacyLockPatternFilename(userId)); 281 } 282 283 private boolean hasFile(String name) { 284 byte[] contents = readFile(name); 285 return contents != null && contents.length > 0; 286 } 287 288 private byte[] readFile(String name) { 289 int version; 290 synchronized (mCache) { 291 if (mCache.hasFile(name)) { 292 return mCache.peekFile(name); 293 } 294 version = mCache.getVersion(); 295 } 296 297 RandomAccessFile raf = null; 298 byte[] stored = null; 299 try { 300 raf = new RandomAccessFile(name, "r"); 301 stored = new byte[(int) raf.length()]; 302 raf.readFully(stored, 0, stored.length); 303 raf.close(); 304 } catch (IOException e) { 305 Slog.e(TAG, "Cannot read file " + e); 306 } finally { 307 if (raf != null) { 308 try { 309 raf.close(); 310 } catch (IOException e) { 311 Slog.e(TAG, "Error closing file " + e); 312 } 313 } 314 } 315 mCache.putFileIfUnchanged(name, stored, version); 316 return stored; 317 } 318 319 private void writeFile(String name, byte[] hash) { 320 synchronized (mFileWriteLock) { 321 RandomAccessFile raf = null; 322 try { 323 // Write the hash to file 324 raf = new RandomAccessFile(name, "rw"); 325 // Truncate the file if pattern is null, to clear the lock 326 if (hash == null || hash.length == 0) { 327 raf.setLength(0); 328 } else { 329 raf.write(hash, 0, hash.length); 330 } 331 raf.close(); 332 } catch (IOException e) { 333 Slog.e(TAG, "Error writing to file " + e); 334 } finally { 335 if (raf != null) { 336 try { 337 raf.close(); 338 } catch (IOException e) { 339 Slog.e(TAG, "Error closing file " + e); 340 } 341 } 342 } 343 mCache.putFile(name, hash); 344 } 345 } 346 347 private void deleteFile(String name) { 348 if (DEBUG) Slog.e(TAG, "Delete file " + name); 349 synchronized (mFileWriteLock) { 350 File file = new File(name); 351 if (file.exists()) { 352 file.delete(); 353 mCache.putFile(name, null); 354 } 355 } 356 } 357 358 public void writePatternHash(byte[] hash, int userId) { 359 mStoredCredentialType.put(userId, hash == null ? CredentialHash.TYPE_NONE 360 : CredentialHash.TYPE_PATTERN); 361 writeFile(getLockPatternFilename(userId), hash); 362 clearPasswordHash(userId); 363 } 364 365 private void clearPatternHash(int userId) { 366 writeFile(getLockPatternFilename(userId), null); 367 } 368 369 public void writePasswordHash(byte[] hash, int userId) { 370 mStoredCredentialType.put(userId, hash == null ? CredentialHash.TYPE_NONE 371 : CredentialHash.TYPE_PASSWORD); 372 writeFile(getLockPasswordFilename(userId), hash); 373 clearPatternHash(userId); 374 } 375 376 private void clearPasswordHash(int userId) { 377 writeFile(getLockPasswordFilename(userId), null); 378 } 379 380 @VisibleForTesting 381 String getLockPatternFilename(int userId) { 382 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE); 383 } 384 385 @VisibleForTesting 386 String getLockPasswordFilename(int userId) { 387 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE); 388 } 389 390 @VisibleForTesting 391 String getLegacyLockPatternFilename(int userId) { 392 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE); 393 } 394 395 @VisibleForTesting 396 String getLegacyLockPasswordFilename(int userId) { 397 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE); 398 } 399 400 private String getBaseZeroLockPatternFilename(int userId) { 401 return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE); 402 } 403 404 @VisibleForTesting 405 String getChildProfileLockFile(int userId) { 406 return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE); 407 } 408 409 private String getLockCredentialFilePathForUser(int userId, String basename) { 410 String dataSystemDirectory = 411 android.os.Environment.getDataDirectory().getAbsolutePath() + 412 SYSTEM_DIRECTORY; 413 if (userId == 0) { 414 // Leave it in the same place for user 0 415 return dataSystemDirectory + basename; 416 } else { 417 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath(); 418 } 419 } 420 421 public void removeUser(int userId) { 422 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 423 424 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE); 425 final UserInfo parentInfo = um.getProfileParent(userId); 426 427 if (parentInfo == null) { 428 // This user owns its lock settings files - safe to delete them 429 synchronized (mFileWriteLock) { 430 String name = getLockPasswordFilename(userId); 431 File file = new File(name); 432 if (file.exists()) { 433 file.delete(); 434 mCache.putFile(name, null); 435 } 436 name = getLockPatternFilename(userId); 437 file = new File(name); 438 if (file.exists()) { 439 file.delete(); 440 mCache.putFile(name, null); 441 } 442 } 443 } else { 444 // Manged profile 445 removeChildProfileLock(userId); 446 } 447 448 try { 449 db.beginTransaction(); 450 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null); 451 db.setTransactionSuccessful(); 452 mCache.removeUser(userId); 453 } finally { 454 db.endTransaction(); 455 } 456 } 457 458 @VisibleForTesting 459 void closeDatabase() { 460 mOpenHelper.close(); 461 } 462 463 @VisibleForTesting 464 void clearCache() { 465 mCache.clear(); 466 } 467 468 public interface Callback { 469 void initialize(SQLiteDatabase db); 470 } 471 472 class DatabaseHelper extends SQLiteOpenHelper { 473 private static final String TAG = "LockSettingsDB"; 474 private static final String DATABASE_NAME = "locksettings.db"; 475 476 private static final int DATABASE_VERSION = 2; 477 478 private final Callback mCallback; 479 480 public DatabaseHelper(Context context, Callback callback) { 481 super(context, DATABASE_NAME, null, DATABASE_VERSION); 482 setWriteAheadLoggingEnabled(true); 483 mCallback = callback; 484 } 485 486 private void createTable(SQLiteDatabase db) { 487 db.execSQL("CREATE TABLE " + TABLE + " (" + 488 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 489 COLUMN_KEY + " TEXT," + 490 COLUMN_USERID + " INTEGER," + 491 COLUMN_VALUE + " TEXT" + 492 ");"); 493 } 494 495 @Override 496 public void onCreate(SQLiteDatabase db) { 497 createTable(db); 498 mCallback.initialize(db); 499 } 500 501 @Override 502 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { 503 int upgradeVersion = oldVersion; 504 if (upgradeVersion == 1) { 505 // Previously migrated lock screen widget settings. Now defunct. 506 upgradeVersion = 2; 507 } 508 509 if (upgradeVersion != DATABASE_VERSION) { 510 Log.w(TAG, "Failed to upgrade database!"); 511 } 512 } 513 } 514 515 /** 516 * Cache consistency model: 517 * - Writes to storage write directly to the cache, but this MUST happen within the atomic 518 * section either provided by the database transaction or mWriteLock, such that writes to the 519 * cache and writes to the backing storage are guaranteed to occur in the same order 520 * 521 * - Reads can populate the cache, but because they are no strong ordering guarantees with 522 * respect to writes this precaution is taken: 523 * - The cache is assigned a version number that increases every time the cache is modified. 524 * Reads from backing storage can only populate the cache if the backing storage 525 * has not changed since the load operation has begun. 526 * This guarantees that no read operation can shadow a write to the cache that happens 527 * after it had begun. 528 */ 529 private static class Cache { 530 private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>(); 531 private final CacheKey mCacheKey = new CacheKey(); 532 private int mVersion = 0; 533 534 String peekKeyValue(String key, String defaultValue, int userId) { 535 Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId); 536 return cached == DEFAULT ? defaultValue : (String) cached; 537 } 538 539 boolean hasKeyValue(String key, int userId) { 540 return contains(CacheKey.TYPE_KEY_VALUE, key, userId); 541 } 542 543 void putKeyValue(String key, String value, int userId) { 544 put(CacheKey.TYPE_KEY_VALUE, key, value, userId); 545 } 546 547 void putKeyValueIfUnchanged(String key, Object value, int userId, int version) { 548 putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version); 549 } 550 551 byte[] peekFile(String fileName) { 552 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */); 553 } 554 555 boolean hasFile(String fileName) { 556 return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */); 557 } 558 559 void putFile(String key, byte[] value) { 560 put(CacheKey.TYPE_FILE, key, value, -1 /* userId */); 561 } 562 563 void putFileIfUnchanged(String key, byte[] value, int version) { 564 putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version); 565 } 566 567 void setFetched(int userId) { 568 put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId); 569 } 570 571 boolean isFetched(int userId) { 572 return contains(CacheKey.TYPE_FETCHED, "", userId); 573 } 574 575 576 private synchronized void put(int type, String key, Object value, int userId) { 577 // Create a new CachKey here because it may be saved in the map if the key is absent. 578 mCache.put(new CacheKey().set(type, key, userId), value); 579 mVersion++; 580 } 581 582 private synchronized void putIfUnchanged(int type, String key, Object value, int userId, 583 int version) { 584 if (!contains(type, key, userId) && mVersion == version) { 585 put(type, key, value, userId); 586 } 587 } 588 589 private synchronized boolean contains(int type, String key, int userId) { 590 return mCache.containsKey(mCacheKey.set(type, key, userId)); 591 } 592 593 private synchronized Object peek(int type, String key, int userId) { 594 return mCache.get(mCacheKey.set(type, key, userId)); 595 } 596 597 private synchronized int getVersion() { 598 return mVersion; 599 } 600 601 synchronized void removeUser(int userId) { 602 for (int i = mCache.size() - 1; i >= 0; i--) { 603 if (mCache.keyAt(i).userId == userId) { 604 mCache.removeAt(i); 605 } 606 } 607 608 // Make sure in-flight loads can't write to cache. 609 mVersion++; 610 } 611 612 synchronized void clear() { 613 mCache.clear(); 614 mVersion++; 615 } 616 617 private static final class CacheKey { 618 static final int TYPE_KEY_VALUE = 0; 619 static final int TYPE_FILE = 1; 620 static final int TYPE_FETCHED = 2; 621 622 String key; 623 int userId; 624 int type; 625 626 public CacheKey set(int type, String key, int userId) { 627 this.type = type; 628 this.key = key; 629 this.userId = userId; 630 return this; 631 } 632 633 @Override 634 public boolean equals(Object obj) { 635 if (!(obj instanceof CacheKey)) 636 return false; 637 CacheKey o = (CacheKey) obj; 638 return userId == o.userId && type == o.type && key.equals(o.key); 639 } 640 641 @Override 642 public int hashCode() { 643 return key.hashCode() ^ userId ^ type; 644 } 645 } 646 } 647 } 648