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