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