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